ph-clm 0.9.0__tar.gz
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.
- ph_clm-0.9.0/.gitignore +31 -0
- ph_clm-0.9.0/LICENSE +21 -0
- ph_clm-0.9.0/PKG-INFO +106 -0
- ph_clm-0.9.0/README.md +80 -0
- ph_clm-0.9.0/pyproject.toml +49 -0
- ph_clm-0.9.0/src/ph_clm/__init__.py +27 -0
- ph_clm-0.9.0/src/ph_clm/budget.py +120 -0
- ph_clm-0.9.0/src/ph_clm/bundle.yaml +14 -0
- ph_clm-0.9.0/src/ph_clm/edits.py +722 -0
- ph_clm-0.9.0/src/ph_clm/keys.py +23 -0
- ph_clm-0.9.0/src/ph_clm/kinds.py +37 -0
- ph_clm-0.9.0/src/ph_clm/mirror.py +672 -0
- ph_clm-0.9.0/src/ph_clm/py.typed +0 -0
- ph_clm-0.9.0/src/ph_clm/sections.py +386 -0
- ph_clm-0.9.0/src/ph_clm/tools.py +585 -0
- ph_clm-0.9.0/tests/clm_helpers.py +175 -0
- ph_clm-0.9.0/tests/test_budget.py +242 -0
- ph_clm-0.9.0/tests/test_clm.py +711 -0
- ph_clm-0.9.0/tests/test_mirror.py +550 -0
ph_clm-0.9.0/.gitignore
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Build and environment
|
|
2
|
+
.venv/
|
|
3
|
+
dist/
|
|
4
|
+
build/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
jjt/
|
|
9
|
+
w2/
|
|
10
|
+
|
|
11
|
+
# Tooling caches
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
# Written by the guest's subprocess collectors and combined at the end of the
|
|
17
|
+
# run; see `conftest.GUEST_COVERAGE_RC`.
|
|
18
|
+
.coverage-guest*
|
|
19
|
+
htmlcov/
|
|
20
|
+
# Dropped at the repo root by pytest-textual-snapshot when a snapshot test
|
|
21
|
+
# fails. The reference snapshots under `__snapshots__/` are the committed
|
|
22
|
+
# expectation; this is the diff viewer for a run that did not match one.
|
|
23
|
+
snapshot_report.html
|
|
24
|
+
|
|
25
|
+
# Reference checkouts of the upstream projects this port reads from. Vendored
|
|
26
|
+
# locally so the plans' citations are verifiable; never part of this repo.
|
|
27
|
+
sources/
|
|
28
|
+
|
|
29
|
+
# Local scratch
|
|
30
|
+
.ph/
|
|
31
|
+
*.local.yaml
|
ph_clm-0.9.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Charles Tabor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ph_clm-0.9.0/PKG-INFO
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ph-clm
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: pH plugin: the model edits its own context — tombstone, replace and rewrite sections of the surface, with the originals kept in the log.
|
|
5
|
+
Project-URL: Homepage, https://github.com/chastabor/pH
|
|
6
|
+
Project-URL: Repository, https://github.com/chastabor/pH
|
|
7
|
+
Project-URL: Documentation, https://github.com/chastabor/pH/blob/main/docs/README.md
|
|
8
|
+
Project-URL: Issues, https://github.com/chastabor/pH/issues
|
|
9
|
+
Author: Charles Tabor
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,clm,context-management,llm
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: ph-core==0.9.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# ph-clm
|
|
28
|
+
|
|
29
|
+
*The model edits its own context: tombstone, replace and rewrite sections of what
|
|
30
|
+
it sees, with every original kept in the log.*
|
|
31
|
+
|
|
32
|
+
After [Context Language Models][clm]: a model keeps the context it works best with
|
|
33
|
+
when it may edit that context itself. In pH an edit never rewrites the log. Each
|
|
34
|
+
one is a surface `replace` — the mechanism compaction and paste offload already
|
|
35
|
+
use — so the model's view changes, the log keeps the originals, and the person's
|
|
36
|
+
transcript still shows the conversation they had.
|
|
37
|
+
|
|
38
|
+
## Sections
|
|
39
|
+
|
|
40
|
+
The model's context is cut into **sections** where no tool call is outstanding: a
|
|
41
|
+
user message, an assistant reply, or an assistant message with every result it
|
|
42
|
+
asked for. Each is named by the seq of its first event (`S412`), which never
|
|
43
|
+
changes, so an id read on one call is good on the next.
|
|
44
|
+
|
|
45
|
+
## Tools
|
|
46
|
+
|
|
47
|
+
| tool | does |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `context_sections` | the map: each section's id, kind, size, and how much follows it |
|
|
50
|
+
| `context_tombstone` | replace a run of sections with a one-line marker saying why |
|
|
51
|
+
| `context_replace` | replace a run of sections with text the model wrote — a summary, or a rewrite |
|
|
52
|
+
| `context_rewrite` | change one passage inside one section's tool output or reply |
|
|
53
|
+
| `context_restore` | undo a section's latest edit, one level: a rewrite exactly, a removed or replaced run as the text it held |
|
|
54
|
+
| `context_recall` | read back the originals a revised section stands for |
|
|
55
|
+
| `context_diff` | a unified diff of a section against the originals it stands for, after the edits that led there |
|
|
56
|
+
|
|
57
|
+
Every edit reports what it costs: a provider's prefix cache cannot serve anything
|
|
58
|
+
after the first change, so the sections *after* an edit are read again once.
|
|
59
|
+
|
|
60
|
+
## Readouts
|
|
61
|
+
|
|
62
|
+
When a tool result takes the context past a share of its window, that result ends
|
|
63
|
+
with one line giving the context's size and how to free space. Each share is reported
|
|
64
|
+
once, and again only after an edit brings the context back below it. The shares are
|
|
65
|
+
the `clm-context` row's `remindAt`, `[0.5, 0.75]` by default; `[]` turns readouts off.
|
|
66
|
+
With compaction mounted, the last reminder comes `lastMargin` (0.2) below its trigger
|
|
67
|
+
instead, 65% under the default 0.85, so the model has room to act before a summary is
|
|
68
|
+
written for it. Under Code Mode the readout spells the call out, keywords and all.
|
|
69
|
+
|
|
70
|
+
## The context file
|
|
71
|
+
|
|
72
|
+
The `clm-mirror` row writes the model's context to a file in the agent's workspace
|
|
73
|
+
scratch before each request, one `[[SECTION S<id>]]` block per section. The model
|
|
74
|
+
edits it with whatever it already uses: `edit`, `sed`, Python in a cell. When the call
|
|
75
|
+
that touched the file finishes, its changes land through the same editor the tools
|
|
76
|
+
use, in one batch, before the call's result. The result carries the receipt.
|
|
77
|
+
|
|
78
|
+
| in the file | becomes |
|
|
79
|
+
|---|---|
|
|
80
|
+
| a section deleted | a tombstone |
|
|
81
|
+
| a tool result's text changed | that result rewritten in place |
|
|
82
|
+
| a step's result lines removed | the step replaced by the text left |
|
|
83
|
+
| a message's text changed | a replacement (a reply is rewritten in place) |
|
|
84
|
+
| no section lines left | one replacement for everything editable |
|
|
85
|
+
|
|
86
|
+
A revised section's header line says what it stands for. The text under it is only
|
|
87
|
+
what the model may edit.
|
|
88
|
+
|
|
89
|
+
A reorder, a damaged header, an edit to a protected section, or a file written before
|
|
90
|
+
the context was last revised is refused. The call's result says why, and the file is
|
|
91
|
+
rewritten.
|
|
92
|
+
|
|
93
|
+
The file is a whole conversation, so it is kept private and short-lived:
|
|
94
|
+
- its directory is 0700 and the file 0600;
|
|
95
|
+
- it is deleted when the session leaves the store;
|
|
96
|
+
- a link put in its place is replaced, never read through.
|
|
97
|
+
|
|
98
|
+
Only an edit a tool call makes is applied. A file changed outside any call (in an
|
|
99
|
+
editor, or by another process) is recorded as declined and rewritten before the next
|
|
100
|
+
request.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
phern --profile rlm-clm
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
[clm]: https://arxiv.org/abs/2609.37725
|
ph_clm-0.9.0/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# ph-clm
|
|
2
|
+
|
|
3
|
+
*The model edits its own context: tombstone, replace and rewrite sections of what
|
|
4
|
+
it sees, with every original kept in the log.*
|
|
5
|
+
|
|
6
|
+
After [Context Language Models][clm]: a model keeps the context it works best with
|
|
7
|
+
when it may edit that context itself. In pH an edit never rewrites the log. Each
|
|
8
|
+
one is a surface `replace` — the mechanism compaction and paste offload already
|
|
9
|
+
use — so the model's view changes, the log keeps the originals, and the person's
|
|
10
|
+
transcript still shows the conversation they had.
|
|
11
|
+
|
|
12
|
+
## Sections
|
|
13
|
+
|
|
14
|
+
The model's context is cut into **sections** where no tool call is outstanding: a
|
|
15
|
+
user message, an assistant reply, or an assistant message with every result it
|
|
16
|
+
asked for. Each is named by the seq of its first event (`S412`), which never
|
|
17
|
+
changes, so an id read on one call is good on the next.
|
|
18
|
+
|
|
19
|
+
## Tools
|
|
20
|
+
|
|
21
|
+
| tool | does |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `context_sections` | the map: each section's id, kind, size, and how much follows it |
|
|
24
|
+
| `context_tombstone` | replace a run of sections with a one-line marker saying why |
|
|
25
|
+
| `context_replace` | replace a run of sections with text the model wrote — a summary, or a rewrite |
|
|
26
|
+
| `context_rewrite` | change one passage inside one section's tool output or reply |
|
|
27
|
+
| `context_restore` | undo a section's latest edit, one level: a rewrite exactly, a removed or replaced run as the text it held |
|
|
28
|
+
| `context_recall` | read back the originals a revised section stands for |
|
|
29
|
+
| `context_diff` | a unified diff of a section against the originals it stands for, after the edits that led there |
|
|
30
|
+
|
|
31
|
+
Every edit reports what it costs: a provider's prefix cache cannot serve anything
|
|
32
|
+
after the first change, so the sections *after* an edit are read again once.
|
|
33
|
+
|
|
34
|
+
## Readouts
|
|
35
|
+
|
|
36
|
+
When a tool result takes the context past a share of its window, that result ends
|
|
37
|
+
with one line giving the context's size and how to free space. Each share is reported
|
|
38
|
+
once, and again only after an edit brings the context back below it. The shares are
|
|
39
|
+
the `clm-context` row's `remindAt`, `[0.5, 0.75]` by default; `[]` turns readouts off.
|
|
40
|
+
With compaction mounted, the last reminder comes `lastMargin` (0.2) below its trigger
|
|
41
|
+
instead, 65% under the default 0.85, so the model has room to act before a summary is
|
|
42
|
+
written for it. Under Code Mode the readout spells the call out, keywords and all.
|
|
43
|
+
|
|
44
|
+
## The context file
|
|
45
|
+
|
|
46
|
+
The `clm-mirror` row writes the model's context to a file in the agent's workspace
|
|
47
|
+
scratch before each request, one `[[SECTION S<id>]]` block per section. The model
|
|
48
|
+
edits it with whatever it already uses: `edit`, `sed`, Python in a cell. When the call
|
|
49
|
+
that touched the file finishes, its changes land through the same editor the tools
|
|
50
|
+
use, in one batch, before the call's result. The result carries the receipt.
|
|
51
|
+
|
|
52
|
+
| in the file | becomes |
|
|
53
|
+
|---|---|
|
|
54
|
+
| a section deleted | a tombstone |
|
|
55
|
+
| a tool result's text changed | that result rewritten in place |
|
|
56
|
+
| a step's result lines removed | the step replaced by the text left |
|
|
57
|
+
| a message's text changed | a replacement (a reply is rewritten in place) |
|
|
58
|
+
| no section lines left | one replacement for everything editable |
|
|
59
|
+
|
|
60
|
+
A revised section's header line says what it stands for. The text under it is only
|
|
61
|
+
what the model may edit.
|
|
62
|
+
|
|
63
|
+
A reorder, a damaged header, an edit to a protected section, or a file written before
|
|
64
|
+
the context was last revised is refused. The call's result says why, and the file is
|
|
65
|
+
rewritten.
|
|
66
|
+
|
|
67
|
+
The file is a whole conversation, so it is kept private and short-lived:
|
|
68
|
+
- its directory is 0700 and the file 0600;
|
|
69
|
+
- it is deleted when the session leaves the store;
|
|
70
|
+
- a link put in its place is replaced, never read through.
|
|
71
|
+
|
|
72
|
+
Only an edit a tool call makes is applied. A file changed outside any call (in an
|
|
73
|
+
editor, or by another process) is recorded as declined and rewritten before the next
|
|
74
|
+
request.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
phern --profile rlm-clm
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
[clm]: https://arxiv.org/abs/2609.37725
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ph-clm"
|
|
7
|
+
version = "0.9.0"
|
|
8
|
+
description = "pH plugin: the model edits its own context — tombstone, replace and rewrite sections of the surface, with the originals kept in the log."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Charles Tabor" }]
|
|
14
|
+
keywords = ["agent", "llm", "context-management", "clm"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Operating System :: MacOS",
|
|
23
|
+
"Topic :: Software Development",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
# Nothing but the core: every edit is a surface `replace` ph-core already folds,
|
|
28
|
+
# so a log written with this package opens and derives the same context on an
|
|
29
|
+
# install without it.
|
|
30
|
+
dependencies = ["ph-core==0.9.0"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/chastabor/pH"
|
|
34
|
+
Repository = "https://github.com/chastabor/pH"
|
|
35
|
+
Documentation = "https://github.com/chastabor/pH/blob/main/docs/README.md"
|
|
36
|
+
Issues = "https://github.com/chastabor/pH/issues"
|
|
37
|
+
|
|
38
|
+
[project.entry-points."ph.bundles"]
|
|
39
|
+
clm = "ph_clm:BUNDLE"
|
|
40
|
+
|
|
41
|
+
[project.entry-points."ph.plugins"]
|
|
42
|
+
clm-context = "ph_clm.tools:apply"
|
|
43
|
+
clm-mirror = "ph_clm.mirror:apply"
|
|
44
|
+
|
|
45
|
+
[tool.uv.sources]
|
|
46
|
+
ph-core = { workspace = true }
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/ph_clm"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""pH's CLM bundle: the model manages its own context.
|
|
2
|
+
|
|
3
|
+
After *Context Language Models* (Shao et al., 2026): a model keeps the context it
|
|
4
|
+
works best with when it may edit that context itself. Here an edit is never a
|
|
5
|
+
rewrite of the log. Every one is a surface `replace` ph-core already folds — a
|
|
6
|
+
**tombstone** or a **replace** stands in for a run of whole sections, a
|
|
7
|
+
**rewrite** changes one result or reply in place — so the model's view changes,
|
|
8
|
+
the log keeps every original, and `transcript()` still shows the person what
|
|
9
|
+
happened. `context_diff` and `context_recall` read the originals back, and
|
|
10
|
+
`context_restore` undoes a section's latest edit.
|
|
11
|
+
|
|
12
|
+
Ideas from the paper's harness and from `pi-clm`; no code from either — the CLM
|
|
13
|
+
repository is CC BY-NC, and this package is MIT.
|
|
14
|
+
|
|
15
|
+
@module ph_clm
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from . import kinds as kinds
|
|
23
|
+
|
|
24
|
+
BUNDLE = Path(__file__).parent / "bundle.yaml"
|
|
25
|
+
"""The rows the `clm` bundle layers over a profile."""
|
|
26
|
+
|
|
27
|
+
__all__ = ["BUNDLE", "kinds"]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Context readouts: a line on a tool result when the context crosses a share of its
|
|
2
|
+
window, so the model knows to edit before the provider's limit, or compaction, decides
|
|
3
|
+
for it.
|
|
4
|
+
|
|
5
|
+
**On the result, not beside it.** A readout rides on the tool result whose output
|
|
6
|
+
crossed the threshold, appended in `tools/post-execute` as the context file's receipt
|
|
7
|
+
is (pi-clm's `sizeTrailer` does the same). Everything the model is shown is logged
|
|
8
|
+
(I3), and a notice message per threshold would be one more surface node to edit away.
|
|
9
|
+
|
|
10
|
+
**Measured as compaction measures** (`TokenMeter.baseline`): the provider's count of
|
|
11
|
+
the last request — system prompt and tool schemas included — moved by what the
|
|
12
|
+
surface did since, plus the result about to be logged, against the window that request
|
|
13
|
+
was routed with. A receipt's context figures are the same number. No window, no
|
|
14
|
+
readouts: there is no share to report.
|
|
15
|
+
|
|
16
|
+
**The last reminder comes a margin below compaction's trigger**, read from the engine
|
|
17
|
+
(`CompactionSeam.trigger_fraction`), so the model always has room to act before a
|
|
18
|
+
summary is written for it. A fixed last share did not: on a 16k slot, 75% left one
|
|
19
|
+
file read before compaction's 85%, and a live run spent it on a malformed call and
|
|
20
|
+
lost the sections it meant to edit to the summary (`Shares.of`).
|
|
21
|
+
|
|
22
|
+
**Once per crossing, and again after an edit brings the context back down.** Per
|
|
23
|
+
session, `Readouts` keeps how many thresholds the context had reached; a readout fires
|
|
24
|
+
when a result raises that, and each reading sets it, so an edit that frees space lowers
|
|
25
|
+
it and the next crossing is reported again. In memory: after a restart the first
|
|
26
|
+
reading sets it without a readout, so a resumed session is not told again what it was
|
|
27
|
+
told before the restart.
|
|
28
|
+
|
|
29
|
+
@module ph_clm.budget
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from collections.abc import Callable, Sequence
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
|
|
37
|
+
from ph.text import thousands
|
|
38
|
+
|
|
39
|
+
__all__ = ["Readouts", "Shares"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class Shares:
|
|
44
|
+
"""The thresholds one reading is judged against, ascending, as shares of its
|
|
45
|
+
window — and the share at which compaction summarizes on its own, when an engine
|
|
46
|
+
says."""
|
|
47
|
+
|
|
48
|
+
thresholds: tuple[float, ...]
|
|
49
|
+
window: int
|
|
50
|
+
compacts_at: float | None
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def of(
|
|
54
|
+
cls, remind_at: Sequence[float], margin: float, compacts_at: float | None, window: int
|
|
55
|
+
) -> Shares:
|
|
56
|
+
"""`remind_at`, with the last reminder moved to `margin` below compaction's
|
|
57
|
+
trigger: any configured share at or past that point gives way to it. Unchanged
|
|
58
|
+
with no engine, or a margin that would put it at or below zero; empty stays
|
|
59
|
+
empty, which is readouts off."""
|
|
60
|
+
shares = sorted(remind_at)
|
|
61
|
+
# Rounded, so 0.85 - 0.2 is the 0.65 it means rather than 0.6499999999999999.
|
|
62
|
+
last = None if compacts_at is None else round(compacts_at - margin, 6)
|
|
63
|
+
if shares and last is not None and last > 0:
|
|
64
|
+
shares = [*(share for share in shares if share < last), last]
|
|
65
|
+
return cls(tuple(shares), window, compacts_at)
|
|
66
|
+
|
|
67
|
+
def reached(self, tokens: int) -> int:
|
|
68
|
+
"""How many thresholds `tokens` is at or past."""
|
|
69
|
+
return sum(1 for share in self.thresholds if tokens >= share * self.window)
|
|
70
|
+
|
|
71
|
+
def line(self, tokens: int, how: str) -> str:
|
|
72
|
+
"""What a result carries when the context reached `tokens`: its size, what to
|
|
73
|
+
do, and when the next reminder comes — or that this is the last, and what
|
|
74
|
+
happens after it."""
|
|
75
|
+
level = self.reached(tokens)
|
|
76
|
+
share = tokens * 100 // self.window
|
|
77
|
+
head = f"[context: ~{thousands(tokens)} of {thousands(self.window)} tokens ({share}%)."
|
|
78
|
+
if level >= len(self.thresholds):
|
|
79
|
+
after = (
|
|
80
|
+
""
|
|
81
|
+
if self.compacts_at is None
|
|
82
|
+
else f" At {round(self.compacts_at * 100)}% older context is summarized for you."
|
|
83
|
+
)
|
|
84
|
+
return f"{head} This is the last reminder: free space now — {how}.{after}]"
|
|
85
|
+
following = round(self.thresholds[level] * 100)
|
|
86
|
+
return (
|
|
87
|
+
f"{head} Free space when it suits you — {how}. "
|
|
88
|
+
f"The next reminder comes at {following}%.]"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(slots=True)
|
|
93
|
+
class Readouts:
|
|
94
|
+
"""Which thresholds each session's context has reached, and when that rises."""
|
|
95
|
+
|
|
96
|
+
_reached: dict[str, int] = field(default_factory=dict)
|
|
97
|
+
|
|
98
|
+
def crossed(
|
|
99
|
+
self, session_id: str, shares: Shares, before: int, most: int, measure: Callable[[], int]
|
|
100
|
+
) -> int | None:
|
|
101
|
+
"""When a result takes the context from `before` tokens to at most `most`: the
|
|
102
|
+
tokens after, if that reached a threshold this session had not; `None`
|
|
103
|
+
otherwise.
|
|
104
|
+
|
|
105
|
+
`measure` counts the tokens after exactly, and is asked only when the answer
|
|
106
|
+
needs it — when `most` could reach a threshold `before` had not, or when a
|
|
107
|
+
readout fires. Tokenizing a long result is the costly part, and most results
|
|
108
|
+
cross nothing.
|
|
109
|
+
"""
|
|
110
|
+
floor = shares.reached(before)
|
|
111
|
+
known = self._reached.get(session_id, floor)
|
|
112
|
+
after = measure() if shares.reached(most) > floor else None
|
|
113
|
+
level = floor if after is None else shares.reached(after)
|
|
114
|
+
self._reached[session_id] = level
|
|
115
|
+
if level <= known:
|
|
116
|
+
return None
|
|
117
|
+
return measure() if after is None else after
|
|
118
|
+
|
|
119
|
+
def forget(self, session_id: str) -> None:
|
|
120
|
+
self._reached.pop(session_id, None)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# ph-clm: the model edits its own context, over any profile.
|
|
2
|
+
#
|
|
3
|
+
# Two rows: the section map with its tools, and the context file. Every edit is a
|
|
4
|
+
# surface `replace`, so compaction keeps working beside it — it summarizes the
|
|
5
|
+
# surface, revisions included — and stays the backstop when a session outgrows
|
|
6
|
+
# its window anyway.
|
|
7
|
+
|
|
8
|
+
- id: clm-context
|
|
9
|
+
name: clm-context
|
|
10
|
+
|
|
11
|
+
# The context as a file the model edits with its ordinary tools, read back after
|
|
12
|
+
# each call and landed through the same editor the tools use (`CLM`).
|
|
13
|
+
- id: clm-mirror
|
|
14
|
+
name: clm-mirror
|