@@ -19,6 +19,12 @@ Budgets(
1919)
2020```
2121
22+ The character size used for budget comparisons is computed by an allocation-free
23+ estimator (` weaver_kernel.firewall.estimated_size ` ) that walks the structure
24+ rather than serialising it with ` json.dumps ` — so a multi-MB raw result is never
25+ fully serialised just to measure it. The estimate is deterministic and tracks
26+ the serialised length closely; only threshold comparisons depend on it.
27+
2228## Response modes
2329
2430| Mode | What you get | When to use |
@@ -51,6 +57,31 @@ expanded = kernel.expand(handle, query={"fields": ["id", "name"]}, principal=pri
5157expanded = kernel.expand(handle, query = {" filter" : {" status" : " unpaid" }}, principal = principal)
5258```
5359
60+ ### Bounding handle memory by size
61+
62+ The store holds * raw, pre-firewall* datasets, and entry count is a poor proxy
63+ for memory — one deployment's 10k entries are kilobytes, another's are
64+ gigabytes. ` HandleStore ` accepts two optional byte budgets (both ` None ` =
65+ disabled, so default behaviour is unchanged):
66+
67+ ``` python
68+ from weaver_kernel import HandleStore
69+
70+ store = HandleStore(
71+ max_total_bytes = 512 * 1024 * 1024 , # evict oldest-first until within budget
72+ max_entry_bytes = 64 * 1024 * 1024 , # reject a single over-cap payload
73+ )
74+ ```
75+
76+ Sizes are estimated with the same ` estimated_size ` walk used for budgets.
77+ ` max_total_bytes ` evicts oldest-first after each store (never the just-stored
78+ entry); ` max_entry_bytes ` rejects an over-cap payload with ` HandleTooLarge `
79+ rather than truncating it, keeping expansion faithful to the original dataset. A
80+ single entry larger than ` max_total_bytes ` can never fit, so it is rejected the
81+ same way — ` current_bytes ` therefore never exceeds ` max_total_bytes ` . Expanding
82+ an evicted handle raises the usual ` HandleNotFound ` . Tighter budgets mean more
83+ "handle expired/evicted" experiences — tune for your workload.
84+
5485## Redaction
5586
5687When a capability has ` SensitivityTag.PII ` or ` SensitivityTag.PCI ` :
@@ -84,11 +115,18 @@ never becomes a sensitive-data sink (see `docs/security.md`).
84115## Summarization
85116
86117Summaries are produced deterministically:
87- - ** list of dicts** → row count + top keys + numeric stats + categorical distributions
118+ - ** list of dicts** → row count + top keys + numeric stats + categorical/boolean
119+ distributions
88120- ** dict** → key list + per-value type/value
89121- ** string** → truncated to 500 chars
90122- ** other** → repr() truncated to 200 chars
91123
124+ Boolean columns are reported as ` True ` /` False ` counts, never averaged (a ` bool `
125+ is an ` int ` subclass in Python, so "mean of ` is_active ` = 0.7" is nonsense). When
126+ the fact list is capped by ` max_facts ` , the final fact is an explicit omission
127+ marker (` … (N more facts omitted; full data via handle) ` ) so a truncated summary
128+ is never mistaken for a complete one.
129+
92130## Cross-invocation budgets
93131
94132The per-invocation ` Budgets ` above cap a single Frame. A separate
@@ -129,21 +167,27 @@ remaining drops *below* 5% does `handle_only` take over.
129167` budget_remaining ` in the returned ` DryRunResult ` , so callers can preview
130168what their next live invocation would actually return.
131169
132- Plug a different token counter (for example, a ` tiktoken ` -based one) via the
133- ` TokenCounter ` protocol:
170+ The default counter (` default_token_counter ` ) is a character-based
171+ ` len(json.dumps(value)) // 4 ` approximation with no extra dependencies. For real
172+ token counts, install the ` tiktoken ` extra and use the shipped factory:
134173
135174``` python
136- import tiktoken # pip install weaver-kernel[tiktoken]
137- enc = tiktoken.encoding_for_model(" gpt-4o" )
175+ from weaver_kernel.firewall import BudgetManager, make_tiktoken_counter
138176
139- def tiktoken_counter (value ):
140- return len (enc.encode(str (value)))
141-
142- manager = BudgetManager(total_budget = 128_000 , token_counter = tiktoken_counter)
177+ # pip install weaver-kernel[tiktoken]
178+ manager = BudgetManager(
179+ total_budget = 128_000 ,
180+ token_counter = make_tiktoken_counter(), # default cl100k_base
181+ # token_counter=make_tiktoken_counter("o200k_base"), # GPT-4o / o-series
182+ )
143183```
144184
145- The default counter (` default_token_counter ` ) is a character-based
146- ` len(json.dumps(value)) // 4 ` approximation with no extra dependencies.
185+ ` make_tiktoken_counter ` resolves and caches the encoder eagerly, so a missing
186+ extra (` ImportError ` ) or an unknown encoding name (` FirewallError ` ) fails at
187+ construction rather than mid-budgeting. The encoding is explicit because models
188+ tokenize differently — name the one you budget against. ` tiktoken ` is imported
189+ lazily, so ` import weaver_kernel ` never pulls the heavyweight dependency. Any
190+ callable matching the ` TokenCounter ` protocol works too.
147191
148192## Streaming
149193
0 commit comments