contextpress 0.6.0__tar.gz → 0.6.2__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.
Files changed (85) hide show
  1. {contextpress-0.6.0 → contextpress-0.6.2}/.gitignore +4 -0
  2. {contextpress-0.6.0 → contextpress-0.6.2}/CHANGELOG.md +15 -0
  3. {contextpress-0.6.0 → contextpress-0.6.2}/PKG-INFO +22 -1
  4. {contextpress-0.6.0 → contextpress-0.6.2}/README.md +21 -0
  5. {contextpress-0.6.0 → contextpress-0.6.2}/ROADMAP.md +3 -3
  6. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/__init__.py +1 -1
  7. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/core.py +13 -0
  8. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/stats.py +63 -0
  9. contextpress-0.6.2/examples/agent_json_compress.py +46 -0
  10. {contextpress-0.6.0 → contextpress-0.6.2}/examples/structure_and_cost.py +12 -7
  11. {contextpress-0.6.0 → contextpress-0.6.2}/pyproject.toml +1 -1
  12. contextpress-0.6.2/tests/fixtures/chats/09_agent_tool_json.json +22 -0
  13. contextpress-0.6.2/tests/fixtures/chats/10_agent_repeated_logs.json +22 -0
  14. contextpress-0.6.2/tests/fixtures/chats/11_agent_mixed.json +32 -0
  15. {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/README.md +3 -0
  16. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_fixture_chats.py +1 -1
  17. contextpress-0.6.2/tests/test_v061.py +55 -0
  18. contextpress-0.6.2/tests/test_v062.py +120 -0
  19. {contextpress-0.6.0 → contextpress-0.6.2}/AGENTS.md +0 -0
  20. {contextpress-0.6.0 → contextpress-0.6.2}/AUDIT.md +0 -0
  21. {contextpress-0.6.0 → contextpress-0.6.2}/CITATION.cff +0 -0
  22. {contextpress-0.6.0 → contextpress-0.6.2}/CONTRIBUTING.md +0 -0
  23. {contextpress-0.6.0 → contextpress-0.6.2}/LICENSE +0 -0
  24. {contextpress-0.6.0 → contextpress-0.6.2}/NOTICE +0 -0
  25. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/_bootstrap.py +0 -0
  26. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/compression.py +0 -0
  27. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/costs.py +0 -0
  28. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/llm/__init__.py +0 -0
  29. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/llm/_helpers.py +0 -0
  30. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/llm/adapters.py +0 -0
  31. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/llm/base.py +0 -0
  32. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/models.py +0 -0
  33. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/normalizer.py +0 -0
  34. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/pipeline.py +0 -0
  35. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/profiles.py +0 -0
  36. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/py.typed +0 -0
  37. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/registry.py +0 -0
  38. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/__init__.py +0 -0
  39. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/base.py +0 -0
  40. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/budget.py +0 -0
  41. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/filler.py +0 -0
  42. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/recency.py +0 -0
  43. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/repetition.py +0 -0
  44. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/resolution.py +0 -0
  45. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/strategies/structure.py +0 -0
  46. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/text_sim.py +0 -0
  47. {contextpress-0.6.0 → contextpress-0.6.2}/contextpress/warnings_capture.py +0 -0
  48. {contextpress-0.6.0 → contextpress-0.6.2}/examples/agent_pipeline.py +0 -0
  49. {contextpress-0.6.0 → contextpress-0.6.2}/examples/benchmark_presets.py +0 -0
  50. {contextpress-0.6.0 → contextpress-0.6.2}/examples/dry_run_preview.py +0 -0
  51. {contextpress-0.6.0 → contextpress-0.6.2}/examples/estimate_and_stats.py +0 -0
  52. {contextpress-0.6.0 → contextpress-0.6.2}/examples/llm_tier_claude.py +0 -0
  53. {contextpress-0.6.0 → contextpress-0.6.2}/examples/llm_tier_gemini.py +0 -0
  54. {contextpress-0.6.0 → contextpress-0.6.2}/examples/llm_tier_ollama.py +0 -0
  55. {contextpress-0.6.0 → contextpress-0.6.2}/examples/llm_tier_openai.py +0 -0
  56. {contextpress-0.6.0 → contextpress-0.6.2}/examples/pick_preset.py +0 -0
  57. {contextpress-0.6.0 → contextpress-0.6.2}/tests/__init__.py +0 -0
  58. {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/01_filler_heavy.json +0 -0
  59. {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/02_resolution_thread.json +0 -0
  60. {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/03_repetition.json +0 -0
  61. {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/04_long_history.json +0 -0
  62. {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/05_agent_tools.json +0 -0
  63. {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/06_rag_chunks.json +0 -0
  64. {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/07_short_stable.json +0 -0
  65. {contextpress-0.6.0 → contextpress-0.6.2}/tests/fixtures/chats/08_mixed_ack_resolution.json +0 -0
  66. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_budget.py +0 -0
  67. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_filler.py +0 -0
  68. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_llm_helpers.py +0 -0
  69. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_models.py +0 -0
  70. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_normalizer.py +0 -0
  71. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_pipeline.py +0 -0
  72. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_recency.py +0 -0
  73. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_repetition.py +0 -0
  74. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_resolution.py +0 -0
  75. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_stats.py +0 -0
  76. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v03.py +0 -0
  77. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v04.py +0 -0
  78. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v05.py +0 -0
  79. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v051.py +0 -0
  80. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v052.py +0 -0
  81. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v053.py +0 -0
  82. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v054.py +0 -0
  83. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v056.py +0 -0
  84. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v058.py +0 -0
  85. {contextpress-0.6.0 → contextpress-0.6.2}/tests/test_v060.py +0 -0
@@ -32,3 +32,7 @@ Thumbs.db
32
32
 
33
33
  # Secrets (never commit)
34
34
  .env
35
+
36
+ # Draft articles / writing prompts (local only)
37
+ articles/STYLE_PROMPT_taha_azizi.md
38
+ articles/introducing-contextpress.md
@@ -4,6 +4,21 @@ All notable changes to `contextpress` are recorded here.
4
4
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.6.2] - 2026-07-27
8
+
9
+ - **Agent-oriented fixtures** — three offline agent threads under `tests/fixtures/chats/`
10
+ (large tool JSON, repeated log lines, mixed tool call/result trace).
11
+ - **`CompressionStats.summary()`** / **`CompressionResult.summary()`** — one-line human-readable
12
+ savings report (tokens, stages, optional USD when ``cost_provider`` is set).
13
+ - Example: `examples/agent_json_compress.py`.
14
+
15
+ ## [0.6.1] - 2026-07-26
16
+
17
+ - **USD on stats** — ``CompressionStats.attach_cost()`` and optional ``cost_provider`` on
18
+ ``ContextManager`` / ``compress()`` fill ``estimated_input_cost_*_usd`` and
19
+ ``estimated_cost_saved_usd`` (included in ``to_dict()``).
20
+ - Opt-in only: without ``cost_provider``, cost fields stay ``None``.
21
+
7
22
  ## [0.6.0] - 2026-07-26
8
23
 
9
24
  - **`structure` stage** — early Tier 1 compaction: JSON minify, whitespace tighten, consecutive log-line dedupe (stdlib only).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: contextpress
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Deterministic context compression for LLM chat, RAG, and agent pipelines
5
5
  Project-URL: Homepage, https://github.com/Taha-azizi/contextpress
6
6
  Project-URL: Documentation, https://github.com/Taha-azizi/contextpress#readme
@@ -415,6 +415,27 @@ est = cm.estimate_cost(messages, provider="openai", model="gpt-4o-mini", output_
415
415
  print(est.total_cost_usd, est.to_dict())
416
416
  ```
417
417
 
418
+ **USD on compression stats** (0.6.1+, opt-in):
419
+
420
+ ```python
421
+ cm = ContextManager(type="chat", model="gpt-4o-mini", cost_provider="openai")
422
+ result = cm.compress(messages, token_budget=2000, return_stats=True)
423
+ print(result.stats.estimated_input_cost_before_usd)
424
+ print(result.stats.estimated_input_cost_after_usd)
425
+ print(result.stats.estimated_cost_saved_usd)
426
+ # or attach later: result.stats.attach_cost(provider="anthropic", model="claude-haiku-4-5")
427
+ ```
428
+
429
+ **Readable savings report** (0.6.2+):
430
+
431
+ ```python
432
+ result = cm.compress(messages, token_budget=2000, return_stats=True)
433
+ print(result.summary())
434
+ # contextpress (chat, medium): 12 -> 8 turns, 842 -> 410 tokens (51.3% saved)
435
+ # stages: structure, filler, repetition, budget
436
+ # est. input cost: $0.000126 -> $0.000061 (saved $0.000065) # when cost_provider set
437
+ ```
438
+
418
439
  See [`ROADMAP.md`](ROADMAP.md) for positioning vs heavier compression stacks and the 0.6.x plan.
419
440
 
420
441
  ## Tier 1 vs Tier 2 (classical NLP vs LLM)
@@ -174,6 +174,27 @@ est = cm.estimate_cost(messages, provider="openai", model="gpt-4o-mini", output_
174
174
  print(est.total_cost_usd, est.to_dict())
175
175
  ```
176
176
 
177
+ **USD on compression stats** (0.6.1+, opt-in):
178
+
179
+ ```python
180
+ cm = ContextManager(type="chat", model="gpt-4o-mini", cost_provider="openai")
181
+ result = cm.compress(messages, token_budget=2000, return_stats=True)
182
+ print(result.stats.estimated_input_cost_before_usd)
183
+ print(result.stats.estimated_input_cost_after_usd)
184
+ print(result.stats.estimated_cost_saved_usd)
185
+ # or attach later: result.stats.attach_cost(provider="anthropic", model="claude-haiku-4-5")
186
+ ```
187
+
188
+ **Readable savings report** (0.6.2+):
189
+
190
+ ```python
191
+ result = cm.compress(messages, token_budget=2000, return_stats=True)
192
+ print(result.summary())
193
+ # contextpress (chat, medium): 12 -> 8 turns, 842 -> 410 tokens (51.3% saved)
194
+ # stages: structure, filler, repetition, budget
195
+ # est. input cost: $0.000126 -> $0.000061 (saved $0.000065) # when cost_provider set
196
+ ```
197
+
177
198
  See [`ROADMAP.md`](ROADMAP.md) for positioning vs heavier compression stacks and the 0.6.x plan.
178
199
 
179
200
  ## Tier 1 vs Tier 2 (classical NLP vs LLM)
@@ -37,8 +37,8 @@ deterministic Tier‑1 NLP for chat / RAG / agent **message histories**, with op
37
37
 
38
38
  | Version | Focus |
39
39
  |---------|--------|
40
- | **0.6.0** | `structure` stage + `estimate_cost()` + this roadmap |
41
- | **0.6.1** | Wire estimated USD into `CompressionStats` / reports |
42
- | **0.6.2** | Agent-oriented fixtures for JSON/tool payloads; polish |
40
+ | **0.6.0** | `structure` stage + `estimate_cost()` + this roadmap — shipped |
41
+ | **0.6.1** | Wire estimated USD into `CompressionStats` / reports — shipped |
42
+ | **0.6.2** | Agent-oriented fixtures for JSON/tool payloads; `summary()` report — shipped |
43
43
 
44
44
  Stay classical-NLP-first; keep optional LLM extras optional.
@@ -16,7 +16,7 @@ __all__ = [
16
16
  "CompressionResult",
17
17
  "CompressionStats",
18
18
  ]
19
- __version__ = "0.6.0"
19
+ __version__ = "0.6.2"
20
20
 
21
21
 
22
22
  def __getattr__(name: str) -> Any:
@@ -47,6 +47,7 @@ class ContextManager:
47
47
  llm_min_input_chars: int = 1500,
48
48
  llm_max_summary_tokens: int = 2048,
49
49
  llm_mode: str = "replace_all",
50
+ cost_provider: str | None = None,
50
51
  ):
51
52
  if type not in PROFILES:
52
53
  raise ValueError(f"unknown context type {type!r}")
@@ -62,6 +63,8 @@ class ContextManager:
62
63
  self.llm_min_input_chars = int(llm_min_input_chars)
63
64
  self.llm_max_summary_tokens = int(llm_max_summary_tokens)
64
65
  self.llm_mode = llm_mode
66
+ # When set, compress(..., return_stats=True) attaches USD fields on stats.
67
+ self.cost_provider = cost_provider
65
68
  self._custom_stages: dict[str, StageConfig] = {}
66
69
 
67
70
  def estimate_tokens(self, messages: Any, *, model: str | None = None) -> int:
@@ -181,12 +184,15 @@ class ContextManager:
181
184
  disable: list[str] | None = None,
182
185
  return_stats: bool = False,
183
186
  dry_run: bool = False,
187
+ cost_provider: str | None = None,
184
188
  ) -> Any | CompressionResult:
185
189
  """Run the pipeline; return value matches input shape (dict list, tuples, strings, etc.).
186
190
 
187
191
  ``token_budget`` must be a positive int or None. Unknown keys in ``disable`` are ignored.
188
192
  With ``return_stats=True``, returns a ``CompressionResult`` with ``messages`` and ``stats``.
189
193
  With ``dry_run=True``, runs Tier 1 only (no LLM calls) and returns the original messages.
194
+ When ``cost_provider`` (or ``self.cost_provider``) is set and stats are returned,
195
+ ``stats`` includes approximate input USD before/after compression.
190
196
  """
191
197
  if dry_run:
192
198
  return_stats = True
@@ -220,6 +226,9 @@ class ContextManager:
220
226
  out = pipeline.run(conv, stats=stats, dry_run=dry_run)
221
227
  if stats is not None:
222
228
  stats.warnings_emitted = captured
229
+ prov = cost_provider if cost_provider is not None else self.cost_provider
230
+ if prov is not None:
231
+ stats.attach_cost(provider=prov, model=self.model or "gpt-4o-mini")
223
232
  if dry_run:
224
233
  messages_out = denormalize_output(clone_conversation(conv), ctx)
225
234
  else:
@@ -239,6 +248,7 @@ class ContextManager:
239
248
  disable: list[str] | None = None,
240
249
  return_stats: bool = False,
241
250
  dry_run: bool = False,
251
+ cost_provider: str | None = None,
242
252
  ) -> list[Any] | list[CompressionResult]:
243
253
  """Run ``compress()`` on each conversation in ``conversations``."""
244
254
  if not isinstance(conversations, list):
@@ -252,6 +262,7 @@ class ContextManager:
252
262
  disable=disable,
253
263
  return_stats=return_stats,
254
264
  dry_run=dry_run,
265
+ cost_provider=cost_provider,
255
266
  )
256
267
  for messages in conversations
257
268
  ]
@@ -266,6 +277,7 @@ class ContextManager:
266
277
  disable: list[str] | None = None,
267
278
  return_stats: bool = False,
268
279
  dry_run: bool = False,
280
+ cost_provider: str | None = None,
269
281
  ) -> Any | CompressionResult:
270
282
  """Async wrapper around ``compress()`` (runs in a worker thread)."""
271
283
  return await asyncio.to_thread(
@@ -277,6 +289,7 @@ class ContextManager:
277
289
  disable=disable,
278
290
  return_stats=return_stats,
279
291
  dry_run=dry_run,
292
+ cost_provider=cost_provider,
280
293
  )
281
294
 
282
295
  def set_compression(self, compression: str) -> None:
@@ -7,6 +7,7 @@ from typing import Any
7
7
 
8
8
  import tiktoken
9
9
 
10
+ from contextpress.costs import estimate_token_cost
10
11
  from contextpress.models import Conversation, Turn
11
12
  from contextpress.normalizer import extract_text_for_processing
12
13
 
@@ -51,6 +52,11 @@ class CompressionStats:
51
52
  token_budget: int | None = None
52
53
  dry_run: bool = False
53
54
  warnings_emitted: list[str] = field(default_factory=list)
55
+ # Optional USD estimates (0.6.1+); filled when cost_provider is set or via attach_cost()
56
+ cost_provider: str | None = None
57
+ cost_model: str | None = None
58
+ estimated_input_cost_before_usd: float | None = None
59
+ estimated_input_cost_after_usd: float | None = None
54
60
 
55
61
  @property
56
62
  def turns_removed(self) -> int:
@@ -66,6 +72,54 @@ class CompressionStats:
66
72
  return 0.0
67
73
  return round(100.0 * self.tokens_saved / self.tokens_before, 2)
68
74
 
75
+ @property
76
+ def estimated_cost_saved_usd(self) -> float | None:
77
+ """Approximate input-USD saved (None if cost was not attached)."""
78
+ before = self.estimated_input_cost_before_usd
79
+ after = self.estimated_input_cost_after_usd
80
+ if before is None or after is None:
81
+ return None
82
+ return round(max(0.0, before - after), 8)
83
+
84
+ def attach_cost(
85
+ self,
86
+ *,
87
+ provider: str = "openai",
88
+ model: str | None = "gpt-4o-mini",
89
+ ) -> CompressionStats:
90
+ """Fill USD fields from ``tokens_before`` / ``tokens_after``. Returns self."""
91
+ before = estimate_token_cost(self.tokens_before, provider=provider, model=model)
92
+ after = estimate_token_cost(self.tokens_after, provider=provider, model=model)
93
+ self.cost_provider = before.provider
94
+ self.cost_model = before.model
95
+ self.estimated_input_cost_before_usd = before.input_cost_usd
96
+ self.estimated_input_cost_after_usd = after.input_cost_usd
97
+ return self
98
+
99
+ def summary(self) -> str:
100
+ """One-line human-readable report for logs, notebooks, and demos."""
101
+ level = self.compression_level or "default"
102
+ dry = " [dry-run]" if self.dry_run else ""
103
+ lines = [
104
+ (
105
+ f"contextpress ({self.context_type}, {level}){dry}: "
106
+ f"{self.turns_before} -> {self.turns_after} turns, "
107
+ f"{self.tokens_before} -> {self.tokens_after} tokens "
108
+ f"({self.token_savings_pct}% saved)"
109
+ )
110
+ ]
111
+ if self.stages_run:
112
+ lines.append(f"stages: {', '.join(self.stages_run)}")
113
+ saved = self.estimated_cost_saved_usd
114
+ before_usd = self.estimated_input_cost_before_usd
115
+ after_usd = self.estimated_input_cost_after_usd
116
+ if saved is not None and before_usd is not None and after_usd is not None:
117
+ lines.append(
118
+ "est. input cost: "
119
+ f"${before_usd:.6f} -> ${after_usd:.6f} (saved ${saved:.6f})"
120
+ )
121
+ return "\n".join(lines)
122
+
69
123
  def to_dict(self) -> dict[str, Any]:
70
124
  """JSON-serializable snapshot of this run."""
71
125
  return {
@@ -86,6 +140,11 @@ class CompressionStats:
86
140
  "token_budget": self.token_budget,
87
141
  "dry_run": self.dry_run,
88
142
  "warnings_emitted": list(self.warnings_emitted),
143
+ "cost_provider": self.cost_provider,
144
+ "cost_model": self.cost_model,
145
+ "estimated_input_cost_before_usd": self.estimated_input_cost_before_usd,
146
+ "estimated_input_cost_after_usd": self.estimated_input_cost_after_usd,
147
+ "estimated_cost_saved_usd": self.estimated_cost_saved_usd,
89
148
  }
90
149
 
91
150
 
@@ -96,6 +155,10 @@ class CompressionResult:
96
155
  messages: Any
97
156
  stats: CompressionStats
98
157
 
158
+ def summary(self) -> str:
159
+ """Human-readable report for this compression run."""
160
+ return self.stats.summary()
161
+
99
162
  def to_dict(self, *, include_messages: bool = True) -> dict[str, Any]:
100
163
  data = {"stats": self.stats.to_dict()}
101
164
  if include_messages:
@@ -0,0 +1,46 @@
1
+ """Agent JSON compression + readable savings report (0.6.2+)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ from contextpress import ContextManager
8
+
9
+ payload = {
10
+ "tool": "search_deploys",
11
+ "service": "api-v2",
12
+ "environment": "staging",
13
+ "events": [
14
+ {
15
+ "id": f"evt-{i:03d}",
16
+ "version": f"2.4.{i % 3}",
17
+ "status": "healthy" if i % 2 else "superseded",
18
+ "details": {"replicas": 3, "region": "us-east-1"},
19
+ }
20
+ for i in range(12)
21
+ ],
22
+ "meta": {"query_ms": 38, "truncated": False},
23
+ }
24
+
25
+ messages = [
26
+ {"role": "system", "content": "You are a deploy agent with tools."},
27
+ {"role": "user", "content": "Summarize recent staging deploys for api-v2."},
28
+ {
29
+ "role": "assistant",
30
+ "content": "Fetching deploy history <tool_call> search_deploys(api-v2, staging)",
31
+ },
32
+ {"role": "user", "content": "Tool result:\n" + json.dumps(payload, indent=2)},
33
+ {"role": "assistant", "content": "Staging has multiple recent releases; latest is healthy."},
34
+ ]
35
+
36
+ cm = ContextManager(
37
+ type="agent",
38
+ model="gpt-4o-mini",
39
+ compression="medium",
40
+ cost_provider="openai",
41
+ )
42
+ result = cm.compress(messages, token_budget=None, return_stats=True)
43
+
44
+ print(result.summary())
45
+ print()
46
+ print("tool result preview:", result.messages[3]["content"][:120], "...")
@@ -18,18 +18,23 @@ messages = [
18
18
  {"role": "assistant", "content": "line\nline\nline\nnext"},
19
19
  ]
20
20
 
21
- cm = ContextManager(type="agent", model="gpt-4o-mini", compression="medium")
21
+ cm = ContextManager(
22
+ type="agent",
23
+ model="gpt-4o-mini",
24
+ compression="medium",
25
+ cost_provider="openai",
26
+ )
22
27
  before = cm.estimate_tokens(messages)
23
- before_cost = cm.estimate_cost(messages, provider="openai")
24
28
  result = cm.compress(messages, token_budget=None, return_stats=True)
25
- after_cost = cm.estimate_cost(result.messages, provider="openai")
29
+ stats = result.stats
26
30
 
27
- print("tokens:", before, "->", result.stats.tokens_after)
31
+ print("tokens:", before, "->", stats.tokens_after)
28
32
  print(
29
33
  "est. input USD:",
30
- f"{before_cost.input_cost_usd:.6f}",
34
+ f"{stats.estimated_input_cost_before_usd:.6f}",
31
35
  "->",
32
- f"{after_cost.input_cost_usd:.6f}",
36
+ f"{stats.estimated_input_cost_after_usd:.6f}",
37
+ f"(saved {stats.estimated_cost_saved_usd:.6f})",
33
38
  )
34
- print("stages:", result.stats.stages_run)
39
+ print("stages:", stats.stages_run)
35
40
  print("user content:", result.messages[1]["content"][:80])
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "contextpress"
7
- version = "0.6.0"
7
+ version = "0.6.2"
8
8
  description = "Deterministic context compression for LLM chat, RAG, and agent pipelines"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "09_agent_tool_json",
3
+ "type": "agent",
4
+ "source": "synthetic (large pretty-printed tool JSON)",
5
+ "messages": [
6
+ {"role": "system", "content": "You are a data agent with search and fetch tools."},
7
+ {"role": "user", "content": "Find recent deploy events for api-v2 on staging."},
8
+ {
9
+ "role": "assistant",
10
+ "content": "Searching deploy logs <tool_call> search_deploys(service=api-v2, env=staging, limit=20)"
11
+ },
12
+ {
13
+ "role": "user",
14
+ "content": "Tool result:\n{\n \"service\": \"api-v2\",\n \"environment\": \"staging\",\n \"events\": [\n {\n \"id\": \"evt-001\",\n \"version\": \"2.4.1\",\n \"status\": \"healthy\",\n \"timestamp\": \"2026-07-25T14:22:11Z\",\n \"details\": {\n \"replicas\": 3,\n \"region\": \"us-east-1\",\n \"notes\": \"Rolling update completed without errors\"\n }\n },\n {\n \"id\": \"evt-002\",\n \"version\": \"2.4.0\",\n \"status\": \"superseded\",\n \"timestamp\": \"2026-07-24T09:10:03Z\",\n \"details\": {\n \"replicas\": 3,\n \"region\": \"us-east-1\",\n \"notes\": \"Previous stable release\"\n }\n },\n {\n \"id\": \"evt-003\",\n \"version\": \"2.3.9\",\n \"status\": \"rolled_back\",\n \"timestamp\": \"2026-07-22T18:44:55Z\",\n \"details\": {\n \"replicas\": 2,\n \"region\": \"us-east-1\",\n \"notes\": \"Rollback after health check failure on pod-2\"\n }\n }\n ],\n \"meta\": {\n \"query_ms\": 42,\n \"truncated\": false,\n \"source\": \"deploy-index\"\n }\n}"
15
+ },
16
+ {
17
+ "role": "assistant",
18
+ "content": "api-v2 staging is on 2.4.1 (healthy). Prior release 2.4.0 was superseded yesterday."
19
+ },
20
+ {"role": "user", "content": "Any blockers for prod?"}
21
+ ]
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "10_agent_repeated_logs",
3
+ "type": "agent",
4
+ "source": "synthetic (agent trace with repeated log lines)",
5
+ "messages": [
6
+ {"role": "system", "content": "You are a debugging agent."},
7
+ {"role": "user", "content": "Why is checkout failing on staging?"},
8
+ {
9
+ "role": "assistant",
10
+ "content": "Pulling recent error logs <tool_call> fetch_logs(service=checkout, env=staging, level=error)"
11
+ },
12
+ {
13
+ "role": "user",
14
+ "content": "Tool result:\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nERROR checkout: payment gateway timeout after 30s\nWARN checkout: retry attempt 3/3\nWARN checkout: retry attempt 3/3\nWARN checkout: retry attempt 3/3\nINFO checkout: circuit breaker opened\nINFO checkout: circuit breaker opened"
15
+ },
16
+ {
17
+ "role": "assistant",
18
+ "content": "Looks like repeated gateway timeouts triggered the circuit breaker."
19
+ },
20
+ {"role": "user", "content": "Summarize the root cause in one sentence."}
21
+ ]
22
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "id": "11_agent_mixed",
3
+ "type": "agent",
4
+ "source": "synthetic (mixed user, tool call, tool result, follow-up)",
5
+ "messages": [
6
+ {"role": "system", "content": "You are an ops agent with deploy and status tools."},
7
+ {"role": "user", "content": "We've decided on using the new pipeline for api-v2 staging deploy."},
8
+ {
9
+ "role": "assistant",
10
+ "content": "Acknowledged. Checking current status <tool_call> get_deploy_status(api-v2, staging)"
11
+ },
12
+ {
13
+ "role": "user",
14
+ "content": "Tool result: {\"service\":\"api-v2\",\"env\":\"staging\",\"version\":\"2.4.1\",\"healthy\":true,\"pods\":[{\"name\":\"api-v2-0\",\"ready\":true},{\"name\":\"api-v2-1\",\"ready\":true}]}"
15
+ },
16
+ {
17
+ "role": "assistant",
18
+ "content": "Staging is healthy on 2.4.1. I can schedule the new pipeline deploy when you confirm."
19
+ },
20
+ {"role": "user", "content": "Go ahead and schedule it for tonight."},
21
+ {
22
+ "role": "assistant",
23
+ "content": "Scheduling deploy <tool_call> schedule_deploy(api-v2, staging, pipeline=new, window=tonight)"
24
+ },
25
+ {
26
+ "role": "user",
27
+ "content": "Tool result:\n{\n \"scheduled\": true,\n \"window\": \"2026-07-26T02:00:00Z\",\n \"pipeline\": \"new\",\n \"ticket\": \"DEP-4821\"\n}"
28
+ },
29
+ {"role": "assistant", "content": "Deploy DEP-4821 is scheduled for tonight via the new pipeline."},
30
+ {"role": "user", "content": "Thanks. Basically that's all I needed."}
31
+ ]
32
+ }
@@ -13,3 +13,6 @@ No network required — used by `tests/test_fixture_chats.py`.
13
13
  | `06_rag_chunks.json` | rag_doc | Chunk + query |
14
14
  | `07_short_stable.json` | chat | Minimal change |
15
15
  | `08_mixed_ack_resolution.json` | chat | Ack vs resolution (AUDIT C3) |
16
+ | `09_agent_tool_json.json` | agent | Large pretty-printed tool JSON |
17
+ | `10_agent_repeated_logs.json` | agent | Repeated log lines in tool output |
18
+ | `11_agent_mixed.json` | agent | Tool call + result + follow-up thread |
@@ -58,7 +58,7 @@ def test_fixture_recommend_preset(path: Path):
58
58
 
59
59
 
60
60
  def test_fixture_catalog_not_empty():
61
- assert len(FIXTURES) >= 8
61
+ assert len(FIXTURES) >= 11
62
62
 
63
63
 
64
64
  def test_c3_ack_may_weaken_resolution_signal():
@@ -0,0 +1,55 @@
1
+ from contextpress import ContextManager
2
+ from contextpress.stats import CompressionStats
3
+
4
+
5
+ def test_attach_cost_on_stats():
6
+ stats = CompressionStats(tokens_before=1_000_000, tokens_after=500_000)
7
+ stats.attach_cost(provider="openai", model="gpt-4o-mini")
8
+ assert stats.cost_provider == "openai"
9
+ assert stats.cost_model == "gpt-4o-mini"
10
+ assert stats.estimated_input_cost_before_usd == 0.15
11
+ assert stats.estimated_input_cost_after_usd == 0.075
12
+ assert stats.estimated_cost_saved_usd == 0.075
13
+ d = stats.to_dict()
14
+ assert d["estimated_cost_saved_usd"] == 0.075
15
+
16
+
17
+ def test_cost_fields_none_by_default():
18
+ cm = ContextManager(type="chat")
19
+ result = cm.compress(
20
+ [{"role": "user", "content": "hello basically"}],
21
+ token_budget=None,
22
+ return_stats=True,
23
+ )
24
+ assert result.stats.cost_provider is None
25
+ assert result.stats.estimated_cost_saved_usd is None
26
+
27
+
28
+ def test_compress_attaches_cost_when_provider_set():
29
+ cm = ContextManager(type="chat", model="gpt-4o-mini", cost_provider="openai")
30
+ messages = [{"role": "user", "content": "basically " + ("word " * 80)}]
31
+ result = cm.compress(messages, token_budget=None, return_stats=True)
32
+ assert result.stats.cost_provider == "openai"
33
+ assert result.stats.estimated_input_cost_before_usd is not None
34
+ assert result.stats.estimated_input_cost_after_usd is not None
35
+ assert result.stats.estimated_cost_saved_usd is not None
36
+ assert result.stats.estimated_cost_saved_usd >= 0
37
+
38
+
39
+ def test_compress_cost_provider_kwarg_overrides_default():
40
+ cm = ContextManager(type="chat", model="gpt-4o-mini", cost_provider="openai")
41
+ result = cm.compress(
42
+ [{"role": "user", "content": "hello " * 40}],
43
+ token_budget=None,
44
+ return_stats=True,
45
+ cost_provider="local",
46
+ )
47
+ assert result.stats.cost_provider == "local"
48
+ assert result.stats.estimated_input_cost_before_usd == 0.0
49
+
50
+
51
+ def test_preview_inherits_cost_provider():
52
+ cm = ContextManager(type="chat", model="gpt-4o-mini", cost_provider="anthropic")
53
+ preview = cm.preview([{"role": "user", "content": "hello " * 20}], token_budget=500)
54
+ assert preview.stats.cost_provider == "anthropic"
55
+ assert preview.stats.estimated_input_cost_before_usd is not None
@@ -0,0 +1,120 @@
1
+ """0.6.2 — agent fixtures, stats.summary(), example script."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from contextpress import ContextManager
9
+ from contextpress.stats import CompressionResult, CompressionStats
10
+
11
+ FIXTURES_DIR = Path(__file__).parent / "fixtures" / "chats"
12
+ AGENT_FIXTURES = [
13
+ FIXTURES_DIR / "09_agent_tool_json.json",
14
+ FIXTURES_DIR / "10_agent_repeated_logs.json",
15
+ FIXTURES_DIR / "11_agent_mixed.json",
16
+ ]
17
+
18
+
19
+ def _load(path: Path) -> dict:
20
+ return json.loads(path.read_text(encoding="utf-8"))
21
+
22
+
23
+ def test_summary_basic():
24
+ stats = CompressionStats(
25
+ context_type="agent",
26
+ compression_level="medium",
27
+ turns_before=8,
28
+ turns_after=6,
29
+ tokens_before=1000,
30
+ tokens_after=500,
31
+ stages_run=["structure", "filler", "repetition"],
32
+ )
33
+ text = stats.summary()
34
+ assert "agent" in text
35
+ assert "medium" in text
36
+ assert "1000 -> 500 tokens" in text
37
+ assert "50.0% saved" in text
38
+ assert "structure" in text
39
+ assert "est. input cost" not in text
40
+
41
+
42
+ def test_summary_includes_cost_when_present():
43
+ stats = CompressionStats(
44
+ context_type="chat",
45
+ compression_level="low",
46
+ turns_before=4,
47
+ turns_after=4,
48
+ tokens_before=200,
49
+ tokens_after=150,
50
+ stages_run=["structure"],
51
+ estimated_input_cost_before_usd=0.000030,
52
+ estimated_input_cost_after_usd=0.0000225,
53
+ )
54
+ text = stats.summary()
55
+ assert "est. input cost:" in text
56
+ assert "saved $0.000008" in text
57
+
58
+
59
+ def test_summary_dry_run_flag():
60
+ stats = CompressionStats(
61
+ context_type="chat",
62
+ dry_run=True,
63
+ turns_before=2,
64
+ turns_after=2,
65
+ tokens_before=50,
66
+ tokens_after=50,
67
+ )
68
+ assert "[dry-run]" in stats.summary()
69
+
70
+
71
+ def test_compression_result_summary_delegates():
72
+ stats = CompressionStats(
73
+ context_type="agent",
74
+ turns_before=3,
75
+ turns_after=3,
76
+ tokens_before=100,
77
+ tokens_after=80,
78
+ )
79
+ result = CompressionResult(messages=[], stats=stats)
80
+ assert result.summary() == stats.summary()
81
+
82
+
83
+ def test_agent_tool_json_fixture_structure_saves_tokens():
84
+ data = _load(AGENT_FIXTURES[0])
85
+ cm = ContextManager(type="agent", compression="medium")
86
+ result = cm.compress(data["messages"], token_budget=None, return_stats=True)
87
+ assert "structure" in result.stats.stages_run
88
+ assert result.stats.tokens_after < result.stats.tokens_before
89
+ texts = " ".join(str(m.get("content", "")) for m in result.messages)
90
+ assert "tool_call" in texts or "api-v2" in texts
91
+
92
+
93
+ def test_agent_repeated_logs_dedupes_lines():
94
+ data = _load(AGENT_FIXTURES[1])
95
+ cm = ContextManager(type="agent", compression="medium")
96
+ result = cm.compress(data["messages"], token_budget=None, return_stats=True)
97
+ assert "structure" in result.stats.stages_run
98
+ tool_turn = next(m for m in result.messages if m.get("role") == "user" and "Tool result" in str(m.get("content", "")))
99
+ content = str(tool_turn["content"])
100
+ assert content.count("payment gateway timeout") == 1
101
+
102
+
103
+ def test_agent_mixed_preserves_tool_markers():
104
+ data = _load(AGENT_FIXTURES[2])
105
+ cm = ContextManager(type="agent", compression="high")
106
+ result = cm.compress(data["messages"], token_budget=600, return_stats=True)
107
+ texts = " ".join(str(m.get("content", "")) for m in result.messages)
108
+ assert "tool_call" in texts or "DEP-4821" in texts or "pipeline" in texts
109
+ assert result.stats.tokens_after <= result.stats.tokens_before
110
+
111
+
112
+ def test_compress_summary_with_cost_provider():
113
+ cm = ContextManager(type="agent", model="gpt-4o-mini", cost_provider="openai")
114
+ messages = [
115
+ {"role": "user", "content": json.dumps({"hits": list(range(50))}, indent=2)},
116
+ ]
117
+ result = cm.compress(messages, token_budget=None, return_stats=True)
118
+ summary = result.summary()
119
+ assert "est. input cost:" in summary
120
+ assert result.stats.estimated_cost_saved_usd is not None
File without changes
File without changes
File without changes
File without changes
File without changes