pyagent-context 0.1.0__tar.gz → 0.2.3__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 (23) hide show
  1. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/PKG-INFO +196 -2
  2. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/README.md +195 -1
  3. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/pyproject.toml +1 -1
  4. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/compression.py +3 -7
  5. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/item.py +10 -5
  6. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/ledger.py +6 -6
  7. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/lifecycle.py +5 -2
  8. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/memory/__init__.py +2 -2
  9. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/memory/semantic.py +106 -19
  10. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/memory/session.py +0 -1
  11. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/memory/working.py +3 -1
  12. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/redaction.py +1 -1
  13. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/retrieval.py +8 -7
  14. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/test_compression.py +37 -27
  15. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/test_ledger.py +4 -2
  16. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/test_lifecycle.py +36 -26
  17. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/test_memory.py +10 -6
  18. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/test_retrieval.py +23 -17
  19. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/.gitignore +0 -0
  20. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/__init__.py +0 -0
  21. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/src/pyagent_context/py.typed +0 -0
  22. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/__init__.py +0 -0
  23. {pyagent_context-0.1.0 → pyagent_context-0.2.3}/tests/test_item.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagent-context
3
- Version: 0.1.0
3
+ Version: 0.2.3
4
4
  Summary: Three-tier memory with trust-aware context ledger for multi-agent LLM systems
5
5
  Project-URL: Homepage, https://pyagent.org
6
6
  Project-URL: Repository, https://github.com/pyagent-core/pyagent
@@ -281,6 +281,200 @@ context_messages = ledger.to_messages(max_tokens=2000)
281
281
  ledger.add(result.output, "pipeline", TrustLevel.INFERRED)
282
282
  ```
283
283
 
284
+ ## Architecture — Three-Tier Memory Model
285
+
286
+ ```mermaid
287
+ flowchart TD
288
+ subgraph Agent Interaction
289
+ A1[Agent 1] -->|append output| CL[ContextLedger]
290
+ CL -->|to_messages budget| A2[Agent 2 input]
291
+ end
292
+
293
+ subgraph Three-Tier Memory
294
+ CL --> WM[WorkingMemory — bounded deque, current turn]
295
+ CL --> SM[SessionMemory — JSON/SQLite, cross-turn persistence]
296
+ CL --> SEM[SemanticMemory — TF-IDF similarity, long-term recall]
297
+ end
298
+
299
+ subgraph Processing
300
+ CL --> CC[ContextCompressor — policy-based trimming]
301
+ CL --> TR[TrustAwareRetriever — ranked retrieval]
302
+ CL --> CR[ContextRedactor — sensitivity filtering]
303
+ CL --> LC[ContextLifecycle — expiry, decay, consolidation]
304
+ end
305
+ ```
306
+
307
+ ### Memory Tier Details
308
+
309
+ | Tier | Class | Storage | Capacity | Eviction | Use Case |
310
+ |------|-------|---------|----------|----------|----------|
311
+ | **Working** | `WorkingMemory` | In-memory deque | `max_items` or `max_tokens` | Oldest-first when full | Current conversation turn |
312
+ | **Session** | `SessionMemory` | JSON file or SQLite | Unlimited | Manual clear | Cross-turn persistence (multi-step workflows) |
313
+ | **Semantic** | `InMemorySemanticStore` | In-memory TF-IDF index | Unlimited | Manual | Long-term recall via similarity search |
314
+
315
+ ### WorkingMemory Eviction
316
+
317
+ When `WorkingMemory` reaches its `max_items` or `max_tokens` limit, the oldest items are evicted first. Utilization metrics are available:
318
+
319
+ ```python
320
+ from pyagent_context import WorkingMemory
321
+
322
+ wm = WorkingMemory(max_items=20, max_tokens=8000)
323
+ wm.add(item)
324
+
325
+ print(wm.utilization) # 0.05 → 5% of max_items used
326
+ print(wm.token_usage) # current token estimate total
327
+ print(len(wm)) # number of items in memory
328
+ wm.clear() # flush all items
329
+ ```
330
+
331
+ ### SessionMemory Backends
332
+
333
+ ```python
334
+ from pyagent_context import SessionMemory
335
+
336
+ # JSON backend — simple file-based persistence
337
+ sm_json = SessionMemory(backend="json", path="session.json")
338
+
339
+ # SQLite backend — more robust, concurrent-safe
340
+ sm_sqlite = SessionMemory(backend="sqlite", path="session.db")
341
+
342
+ sm_json.add(item)
343
+ sm_json.save() # persist to disk
344
+ sm_json.load() # reload from disk
345
+ items = sm_json.retrieve(query="billing", top_k=5)
346
+ ```
347
+
348
+ ### SemanticMemory — TF-IDF Similarity Search
349
+
350
+ The `InMemorySemanticStore` uses TF-IDF vectorization for similarity-based retrieval across the full context history:
351
+
352
+ ```python
353
+ from pyagent_context import InMemorySemanticStore
354
+
355
+ store = InMemorySemanticStore()
356
+ store.add(item1)
357
+ store.add(item2)
358
+ store.add(item3)
359
+
360
+ # Retrieve items most similar to the query
361
+ results = store.search("billing question", top_k=3)
362
+ for item, score in results:
363
+ print(f"[{score:.2f}] {item.content[:80]}...")
364
+ ```
365
+
366
+ ## ContextLedger — Token-Budgeted Message Conversion
367
+
368
+ The `ContextLedger` is an append-only log that converts stored `ContextItem` objects into LLM-compatible messages with automatic token budgeting:
369
+
370
+ ```python
371
+ from pyagent_context import ContextLedger, ContextItem, TrustLevel
372
+
373
+ ledger = ContextLedger()
374
+ ledger.append(ContextItem(content="Revenue was $25.2B", source="database", trust=TrustLevel.VERIFIED))
375
+ ledger.append(ContextItem(content="Margin expanded to 17%", source="agent_1", trust=TrustLevel.INFERRED))
376
+
377
+ # Convert to messages with a token budget
378
+ # Higher-trust items are prioritized when budget is tight
379
+ messages = ledger.to_messages(budget=4000)
380
+ print(ledger.total_tokens()) # total token estimate across all items
381
+ ```
382
+
383
+ ## TrustAwareRetriever — Composite Scoring
384
+
385
+ The retriever ranks items using a composite score of three signals:
386
+
387
+ | Signal | Weight | Description |
388
+ |--------|--------|-------------|
389
+ | **Trust** | Configurable | `VERIFIED` > `INFERRED` > `USER_PROVIDED` > `UNVERIFIED` |
390
+ | **Recency** | Half-life decay | Newer items score higher; decay rate is configurable |
391
+ | **Relevance** | Keyword overlap | TF-IDF similarity between query and item content |
392
+
393
+ ```python
394
+ from pyagent_context import TrustAwareRetriever
395
+
396
+ retriever = TrustAwareRetriever(
397
+ trust_weight=0.4,
398
+ recency_weight=0.3,
399
+ relevance_weight=0.3,
400
+ half_life_hours=24.0,
401
+ )
402
+ results = retriever.retrieve(items, query="billing issue", top_k=5)
403
+ ```
404
+
405
+ ## Integration with pyagent-patterns
406
+
407
+ Context flows between agents via the `ContextLedger`:
408
+
409
+ ```python
410
+ from pyagent_patterns.base import Agent, MockLLM
411
+ from pyagent_patterns.orchestration import Pipeline
412
+ from pyagent_context import ContextLedger, ContextItem, TrustLevel
413
+
414
+ ledger = ContextLedger()
415
+
416
+ # Before agent execution: read context to prepend as system/user messages
417
+ context_messages = ledger.to_messages(budget=4000)
418
+
419
+ # After agent execution: write output as a new context item
420
+ ledger.append(ContextItem(
421
+ content=result.output,
422
+ source="analyst",
423
+ trust=TrustLevel.INFERRED,
424
+ ))
425
+ ```
426
+
427
+ In the hook-based integration model, agents automatically read from and write to the ledger when one is attached via `set_context()`.
428
+
429
+ ## Integration with pyagent-compress
430
+
431
+ Two levels of compression work together:
432
+
433
+ | Layer | Package | What It Compresses | How |
434
+ |-------|---------|-------------------|-----|
435
+ | **Message-level** | `pyagent-compress` | Individual agent outputs | Extractive: remove filler, rank sentences, keep top-N |
436
+ | **Context-level** | `pyagent-context` | Accumulated context items | Policy-based: FIFO, semantic lossless, sawtooth |
437
+
438
+ ```python
439
+ from pyagent_context import ContextCompressor, ContextLedger
440
+ from pyagent_compress import MessageCompressor
441
+
442
+ # Context compression: decide which items to keep
443
+ compressor = ContextCompressor(policy="semantic_lossless")
444
+ trimmed = compressor.compress(ledger.items(), target_tokens=4000)
445
+
446
+ # Message compression: reduce verbosity of individual outputs
447
+ msg_compressor = MessageCompressor(target_ratio=0.5)
448
+ compressed = msg_compressor.compress(agent_output)
449
+ ```
450
+
451
+ ## Integration with pyagent-trace
452
+
453
+ Context operations can be tracked via the `TraceEventBus`:
454
+
455
+ - **Ledger writes** — When agents append items, trace events capture the source, trust level, and token count
456
+ - **Memory tier transitions** — Working → session → semantic migrations emit trace events
457
+ - **Retrieval** — Trust-aware retrieval results (scores, items selected) are logged for debugging
458
+ - **Compression** — Context compression events show which items were kept/dropped and the token savings
459
+
460
+ ## Integration with pyagent-blueprint
461
+
462
+ The `context` section of a blueprint YAML maps directly to context package configuration:
463
+
464
+ ```yaml
465
+ context:
466
+ memory:
467
+ backend: sqlite
468
+ working_max_tokens: 128000
469
+ compression:
470
+ policy: semantic_lossless
471
+ target_ratio: 0.6
472
+ redaction:
473
+ max_sensitivity: internal
474
+ ```
475
+
476
+ After `BlueprintCompiler.compile()`, these settings are available on the `RuntimeGraph` for the consumer to wire into agents.
477
+
284
478
  ## Full Documentation
285
479
 
286
- See [pyagent.dev](https://pyagent.dev) for full API reference and integration guides.
480
+ See [pyagent.org](https://pyagent.org) for full API reference and integration guides.
@@ -250,6 +250,200 @@ context_messages = ledger.to_messages(max_tokens=2000)
250
250
  ledger.add(result.output, "pipeline", TrustLevel.INFERRED)
251
251
  ```
252
252
 
253
+ ## Architecture — Three-Tier Memory Model
254
+
255
+ ```mermaid
256
+ flowchart TD
257
+ subgraph Agent Interaction
258
+ A1[Agent 1] -->|append output| CL[ContextLedger]
259
+ CL -->|to_messages budget| A2[Agent 2 input]
260
+ end
261
+
262
+ subgraph Three-Tier Memory
263
+ CL --> WM[WorkingMemory — bounded deque, current turn]
264
+ CL --> SM[SessionMemory — JSON/SQLite, cross-turn persistence]
265
+ CL --> SEM[SemanticMemory — TF-IDF similarity, long-term recall]
266
+ end
267
+
268
+ subgraph Processing
269
+ CL --> CC[ContextCompressor — policy-based trimming]
270
+ CL --> TR[TrustAwareRetriever — ranked retrieval]
271
+ CL --> CR[ContextRedactor — sensitivity filtering]
272
+ CL --> LC[ContextLifecycle — expiry, decay, consolidation]
273
+ end
274
+ ```
275
+
276
+ ### Memory Tier Details
277
+
278
+ | Tier | Class | Storage | Capacity | Eviction | Use Case |
279
+ |------|-------|---------|----------|----------|----------|
280
+ | **Working** | `WorkingMemory` | In-memory deque | `max_items` or `max_tokens` | Oldest-first when full | Current conversation turn |
281
+ | **Session** | `SessionMemory` | JSON file or SQLite | Unlimited | Manual clear | Cross-turn persistence (multi-step workflows) |
282
+ | **Semantic** | `InMemorySemanticStore` | In-memory TF-IDF index | Unlimited | Manual | Long-term recall via similarity search |
283
+
284
+ ### WorkingMemory Eviction
285
+
286
+ When `WorkingMemory` reaches its `max_items` or `max_tokens` limit, the oldest items are evicted first. Utilization metrics are available:
287
+
288
+ ```python
289
+ from pyagent_context import WorkingMemory
290
+
291
+ wm = WorkingMemory(max_items=20, max_tokens=8000)
292
+ wm.add(item)
293
+
294
+ print(wm.utilization) # 0.05 → 5% of max_items used
295
+ print(wm.token_usage) # current token estimate total
296
+ print(len(wm)) # number of items in memory
297
+ wm.clear() # flush all items
298
+ ```
299
+
300
+ ### SessionMemory Backends
301
+
302
+ ```python
303
+ from pyagent_context import SessionMemory
304
+
305
+ # JSON backend — simple file-based persistence
306
+ sm_json = SessionMemory(backend="json", path="session.json")
307
+
308
+ # SQLite backend — more robust, concurrent-safe
309
+ sm_sqlite = SessionMemory(backend="sqlite", path="session.db")
310
+
311
+ sm_json.add(item)
312
+ sm_json.save() # persist to disk
313
+ sm_json.load() # reload from disk
314
+ items = sm_json.retrieve(query="billing", top_k=5)
315
+ ```
316
+
317
+ ### SemanticMemory — TF-IDF Similarity Search
318
+
319
+ The `InMemorySemanticStore` uses TF-IDF vectorization for similarity-based retrieval across the full context history:
320
+
321
+ ```python
322
+ from pyagent_context import InMemorySemanticStore
323
+
324
+ store = InMemorySemanticStore()
325
+ store.add(item1)
326
+ store.add(item2)
327
+ store.add(item3)
328
+
329
+ # Retrieve items most similar to the query
330
+ results = store.search("billing question", top_k=3)
331
+ for item, score in results:
332
+ print(f"[{score:.2f}] {item.content[:80]}...")
333
+ ```
334
+
335
+ ## ContextLedger — Token-Budgeted Message Conversion
336
+
337
+ The `ContextLedger` is an append-only log that converts stored `ContextItem` objects into LLM-compatible messages with automatic token budgeting:
338
+
339
+ ```python
340
+ from pyagent_context import ContextLedger, ContextItem, TrustLevel
341
+
342
+ ledger = ContextLedger()
343
+ ledger.append(ContextItem(content="Revenue was $25.2B", source="database", trust=TrustLevel.VERIFIED))
344
+ ledger.append(ContextItem(content="Margin expanded to 17%", source="agent_1", trust=TrustLevel.INFERRED))
345
+
346
+ # Convert to messages with a token budget
347
+ # Higher-trust items are prioritized when budget is tight
348
+ messages = ledger.to_messages(budget=4000)
349
+ print(ledger.total_tokens()) # total token estimate across all items
350
+ ```
351
+
352
+ ## TrustAwareRetriever — Composite Scoring
353
+
354
+ The retriever ranks items using a composite score of three signals:
355
+
356
+ | Signal | Weight | Description |
357
+ |--------|--------|-------------|
358
+ | **Trust** | Configurable | `VERIFIED` > `INFERRED` > `USER_PROVIDED` > `UNVERIFIED` |
359
+ | **Recency** | Half-life decay | Newer items score higher; decay rate is configurable |
360
+ | **Relevance** | Keyword overlap | TF-IDF similarity between query and item content |
361
+
362
+ ```python
363
+ from pyagent_context import TrustAwareRetriever
364
+
365
+ retriever = TrustAwareRetriever(
366
+ trust_weight=0.4,
367
+ recency_weight=0.3,
368
+ relevance_weight=0.3,
369
+ half_life_hours=24.0,
370
+ )
371
+ results = retriever.retrieve(items, query="billing issue", top_k=5)
372
+ ```
373
+
374
+ ## Integration with pyagent-patterns
375
+
376
+ Context flows between agents via the `ContextLedger`:
377
+
378
+ ```python
379
+ from pyagent_patterns.base import Agent, MockLLM
380
+ from pyagent_patterns.orchestration import Pipeline
381
+ from pyagent_context import ContextLedger, ContextItem, TrustLevel
382
+
383
+ ledger = ContextLedger()
384
+
385
+ # Before agent execution: read context to prepend as system/user messages
386
+ context_messages = ledger.to_messages(budget=4000)
387
+
388
+ # After agent execution: write output as a new context item
389
+ ledger.append(ContextItem(
390
+ content=result.output,
391
+ source="analyst",
392
+ trust=TrustLevel.INFERRED,
393
+ ))
394
+ ```
395
+
396
+ In the hook-based integration model, agents automatically read from and write to the ledger when one is attached via `set_context()`.
397
+
398
+ ## Integration with pyagent-compress
399
+
400
+ Two levels of compression work together:
401
+
402
+ | Layer | Package | What It Compresses | How |
403
+ |-------|---------|-------------------|-----|
404
+ | **Message-level** | `pyagent-compress` | Individual agent outputs | Extractive: remove filler, rank sentences, keep top-N |
405
+ | **Context-level** | `pyagent-context` | Accumulated context items | Policy-based: FIFO, semantic lossless, sawtooth |
406
+
407
+ ```python
408
+ from pyagent_context import ContextCompressor, ContextLedger
409
+ from pyagent_compress import MessageCompressor
410
+
411
+ # Context compression: decide which items to keep
412
+ compressor = ContextCompressor(policy="semantic_lossless")
413
+ trimmed = compressor.compress(ledger.items(), target_tokens=4000)
414
+
415
+ # Message compression: reduce verbosity of individual outputs
416
+ msg_compressor = MessageCompressor(target_ratio=0.5)
417
+ compressed = msg_compressor.compress(agent_output)
418
+ ```
419
+
420
+ ## Integration with pyagent-trace
421
+
422
+ Context operations can be tracked via the `TraceEventBus`:
423
+
424
+ - **Ledger writes** — When agents append items, trace events capture the source, trust level, and token count
425
+ - **Memory tier transitions** — Working → session → semantic migrations emit trace events
426
+ - **Retrieval** — Trust-aware retrieval results (scores, items selected) are logged for debugging
427
+ - **Compression** — Context compression events show which items were kept/dropped and the token savings
428
+
429
+ ## Integration with pyagent-blueprint
430
+
431
+ The `context` section of a blueprint YAML maps directly to context package configuration:
432
+
433
+ ```yaml
434
+ context:
435
+ memory:
436
+ backend: sqlite
437
+ working_max_tokens: 128000
438
+ compression:
439
+ policy: semantic_lossless
440
+ target_ratio: 0.6
441
+ redaction:
442
+ max_sensitivity: internal
443
+ ```
444
+
445
+ After `BlueprintCompiler.compile()`, these settings are available on the `RuntimeGraph` for the consumer to wire into agents.
446
+
253
447
  ## Full Documentation
254
448
 
255
- See [pyagent.dev](https://pyagent.dev) for full API reference and integration guides.
449
+ See [pyagent.org](https://pyagent.org) for full API reference and integration guides.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyagent-context"
7
- version = "0.1.0"
7
+ version = "0.2.3"
8
8
  description = "Three-tier memory with trust-aware context ledger for multi-agent LLM systems"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -3,22 +3,18 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from enum import StrEnum
6
- from typing import TYPE_CHECKING
7
6
 
8
7
  from pyagent_context.item import ContextItem, TrustLevel
9
8
  from pyagent_context.ledger import ContextLedger
10
9
 
11
- if TYPE_CHECKING:
12
- pass
13
-
14
10
 
15
11
  class CompressionPolicy(StrEnum):
16
12
  """Available compression strategies."""
17
13
 
18
14
  NONE = "none"
19
- FIFO = "fifo" # drop oldest items
20
- SEMANTIC_LOSSLESS = "semantic_lossless" # compress text, keep Knowledge block
21
- SAWTOOTH = "sawtooth" # compress to floor, grow again
15
+ FIFO = "fifo" # drop oldest items
16
+ SEMANTIC_LOSSLESS = "semantic_lossless" # compress text, keep Knowledge block
17
+ SAWTOOTH = "sawtooth" # compress to floor, grow again
22
18
 
23
19
 
24
20
  class ContextCompressor:
@@ -11,13 +11,18 @@ from enum import StrEnum
11
11
  class TrustLevel(StrEnum):
12
12
  """Trust classification for context items."""
13
13
 
14
- VERIFIED = "verified" # from trusted source, validated
15
- INFERRED = "inferred" # LLM-generated, not validated
16
- USER_PROVIDED = "user" # direct user input
17
- EXTERNAL = "external" # from tool/API call
14
+ VERIFIED = "verified" # from trusted source, validated
15
+ INFERRED = "inferred" # LLM-generated, not validated
16
+ USER_PROVIDED = "user" # direct user input
17
+ EXTERNAL = "external" # from tool/API call
18
18
 
19
19
  def __ge__(self, other: TrustLevel) -> bool:
20
- order = {TrustLevel.INFERRED: 0, TrustLevel.EXTERNAL: 1, TrustLevel.USER_PROVIDED: 2, TrustLevel.VERIFIED: 3}
20
+ order = {
21
+ TrustLevel.INFERRED: 0,
22
+ TrustLevel.EXTERNAL: 1,
23
+ TrustLevel.USER_PROVIDED: 2,
24
+ TrustLevel.VERIFIED: 3,
25
+ }
21
26
  return order.get(self, 0) >= order.get(other, 0)
22
27
 
23
28
  def __gt__(self, other: TrustLevel) -> bool:
@@ -6,7 +6,8 @@ import time
6
6
  from typing import Any
7
7
 
8
8
  from pyagent_patterns.base import Message
9
- from pyagent_context.item import ContextItem, TrustLevel, TRUST_ORDER
9
+
10
+ from pyagent_context.item import TRUST_ORDER, ContextItem, TrustLevel
10
11
 
11
12
 
12
13
  class ContextLedger:
@@ -62,7 +63,9 @@ class ContextLedger:
62
63
  now = time.time()
63
64
  results: list[ContextItem] = []
64
65
  for item in self._items:
65
- if min_trust is not None and TRUST_ORDER.get(item.trust_level, 0) < TRUST_ORDER.get(min_trust, 0):
66
+ if min_trust is not None and TRUST_ORDER.get(item.trust_level, 0) < TRUST_ORDER.get(
67
+ min_trust, 0
68
+ ):
66
69
  continue
67
70
  if max_age_seconds is not None and (now - item.timestamp) > max_age_seconds:
68
71
  continue
@@ -89,10 +92,7 @@ class ContextLedger:
89
92
  List of ``Message`` objects.
90
93
  """
91
94
  if max_tokens is None:
92
- return [
93
- Message.assistant(item.content, name=item.source)
94
- for item in self._items
95
- ]
95
+ return [Message.assistant(item.content, name=item.source) for item in self._items]
96
96
 
97
97
  # Walk backward, accumulate until budget exhausted
98
98
  selected: list[ContextItem] = []
@@ -18,7 +18,7 @@ class ContextLifecycle:
18
18
  - **Consolidation**: merge items from the same source with similar content.
19
19
 
20
20
  Args:
21
- consolidation_threshold: Minimum keyword overlap ratio (0.01.0)
21
+ consolidation_threshold: Minimum keyword overlap ratio (0.0-1.0)
22
22
  to consider two items similar enough to merge.
23
23
  """
24
24
 
@@ -123,7 +123,10 @@ class ContextLifecycle:
123
123
  if j in merged_indices:
124
124
  continue
125
125
  item_b = items[j]
126
- if self._similarity(item_a.content, item_b.content) >= self._consolidation_threshold:
126
+ if (
127
+ self._similarity(item_a.content, item_b.content)
128
+ >= self._consolidation_threshold
129
+ ):
127
130
  merged_content = f"{merged_content}\n{item_b.content}"
128
131
  if item_b.trust_level > best_trust:
129
132
  best_trust = item_b.trust_level
@@ -1,8 +1,8 @@
1
1
  """Three-tier memory: working, session, and semantic."""
2
2
 
3
- from pyagent_context.memory.working import WorkingMemory
3
+ from pyagent_context.memory.semantic import InMemorySemanticStore, SemanticMemoryProtocol
4
4
  from pyagent_context.memory.session import SessionMemory
5
- from pyagent_context.memory.semantic import SemanticMemoryProtocol, InMemorySemanticStore
5
+ from pyagent_context.memory.working import WorkingMemory
6
6
 
7
7
  __all__ = [
8
8
  "InMemorySemanticStore",
@@ -4,10 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  import math
6
6
  from collections import Counter
7
- from dataclasses import dataclass, field
8
- from typing import Protocol, runtime_checkable
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
9
9
 
10
- from pyagent_context.item import ContextItem
10
+ if TYPE_CHECKING:
11
+ from pyagent_context.item import ContextItem
11
12
 
12
13
 
13
14
  @dataclass(frozen=True)
@@ -16,7 +17,7 @@ class SearchResult:
16
17
 
17
18
  Attributes:
18
19
  item: The matching context item.
19
- score: Relevance score (0.01.0).
20
+ score: Relevance score (0.0-1.0).
20
21
  """
21
22
 
22
23
  item: ContextItem
@@ -60,18 +61,100 @@ class InMemorySemanticStore:
60
61
  self._items: dict[str, ContextItem] = {}
61
62
  self._tf_cache: dict[str, dict[str, float]] = {}
62
63
  self._stop_words = stop_words or {
63
- "a", "an", "the", "is", "are", "was", "were", "be", "been",
64
- "being", "have", "has", "had", "do", "does", "did", "will",
65
- "would", "could", "should", "may", "might", "can", "shall",
66
- "to", "of", "in", "for", "on", "with", "at", "by", "from",
67
- "as", "into", "through", "during", "before", "after", "and",
68
- "but", "or", "nor", "not", "so", "yet", "both", "either",
69
- "neither", "each", "every", "all", "any", "few", "more",
70
- "most", "other", "some", "such", "no", "only", "own", "same",
71
- "than", "too", "very", "just", "because", "about", "between",
72
- "it", "its", "this", "that", "these", "those", "i", "me",
73
- "my", "we", "our", "you", "your", "he", "him", "his", "she",
74
- "her", "they", "them", "their", "what", "which", "who",
64
+ "a",
65
+ "an",
66
+ "the",
67
+ "is",
68
+ "are",
69
+ "was",
70
+ "were",
71
+ "be",
72
+ "been",
73
+ "being",
74
+ "have",
75
+ "has",
76
+ "had",
77
+ "do",
78
+ "does",
79
+ "did",
80
+ "will",
81
+ "would",
82
+ "could",
83
+ "should",
84
+ "may",
85
+ "might",
86
+ "can",
87
+ "shall",
88
+ "to",
89
+ "of",
90
+ "in",
91
+ "for",
92
+ "on",
93
+ "with",
94
+ "at",
95
+ "by",
96
+ "from",
97
+ "as",
98
+ "into",
99
+ "through",
100
+ "during",
101
+ "before",
102
+ "after",
103
+ "and",
104
+ "but",
105
+ "or",
106
+ "nor",
107
+ "not",
108
+ "so",
109
+ "yet",
110
+ "both",
111
+ "either",
112
+ "neither",
113
+ "each",
114
+ "every",
115
+ "all",
116
+ "any",
117
+ "few",
118
+ "more",
119
+ "most",
120
+ "other",
121
+ "some",
122
+ "such",
123
+ "no",
124
+ "only",
125
+ "own",
126
+ "same",
127
+ "than",
128
+ "too",
129
+ "very",
130
+ "just",
131
+ "because",
132
+ "about",
133
+ "between",
134
+ "it",
135
+ "its",
136
+ "this",
137
+ "that",
138
+ "these",
139
+ "those",
140
+ "i",
141
+ "me",
142
+ "my",
143
+ "we",
144
+ "our",
145
+ "you",
146
+ "your",
147
+ "he",
148
+ "him",
149
+ "his",
150
+ "she",
151
+ "her",
152
+ "they",
153
+ "them",
154
+ "their",
155
+ "what",
156
+ "which",
157
+ "who",
75
158
  }
76
159
 
77
160
  def add(self, item: ContextItem) -> None:
@@ -118,7 +201,11 @@ class InMemorySemanticStore:
118
201
 
119
202
  def _tokenize(self, text: str) -> list[str]:
120
203
  words = text.lower().split()
121
- return [w.strip(".,!?;:\"'()[]{}") for w in words if w.strip(".,!?;:\"'()[]{}") not in self._stop_words]
204
+ return [
205
+ w.strip(".,!?;:\"'()[]{}")
206
+ for w in words
207
+ if w.strip(".,!?;:\"'()[]{}") not in self._stop_words
208
+ ]
122
209
 
123
210
  def _compute_tf(self, text: str) -> dict[str, float]:
124
211
  tokens = self._tokenize(text)
@@ -146,8 +233,8 @@ class InMemorySemanticStore:
146
233
  if not common_keys:
147
234
  return 0.0
148
235
  dot = sum(a[k] * b[k] for k in common_keys)
149
- mag_a = math.sqrt(sum(v ** 2 for v in a.values()))
150
- mag_b = math.sqrt(sum(v ** 2 for v in b.values()))
236
+ mag_a = math.sqrt(sum(v**2 for v in a.values()))
237
+ mag_b = math.sqrt(sum(v**2 for v in b.values()))
151
238
  if mag_a == 0 or mag_b == 0:
152
239
  return 0.0
153
240
  return dot / (mag_a * mag_b)
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  import json
6
6
  import sqlite3
7
7
  from pathlib import Path
8
- from typing import Any
9
8
 
10
9
  from pyagent_context.item import ContextItem
11
10
 
@@ -3,8 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections import deque
6
+ from typing import TYPE_CHECKING
6
7
 
7
- from pyagent_context.item import ContextItem
8
+ if TYPE_CHECKING:
9
+ from pyagent_context.item import ContextItem
8
10
 
9
11
 
10
12
  class WorkingMemory:
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from pyagent_context.item import ContextItem, Sensitivity, SENSITIVITY_ORDER
5
+ from pyagent_context.item import SENSITIVITY_ORDER, ContextItem, Sensitivity
6
6
  from pyagent_context.ledger import ContextLedger
7
7
 
8
8
 
@@ -1,13 +1,16 @@
1
- """TrustAwareRetriever: score candidates by trust × recency × relevance."""
1
+ """TrustAwareRetriever: score candidates by trust x recency x relevance."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import math
6
6
  import time
7
7
  from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING
8
9
 
9
- from pyagent_context.item import ContextItem, TRUST_ORDER
10
- from pyagent_context.ledger import ContextLedger
10
+ from pyagent_context.item import TRUST_ORDER, ContextItem
11
+
12
+ if TYPE_CHECKING:
13
+ from pyagent_context.ledger import ContextLedger
11
14
 
12
15
 
13
16
  @dataclass(frozen=True)
@@ -16,7 +19,7 @@ class ScoredItem:
16
19
 
17
20
  Attributes:
18
21
  item: The context item.
19
- score: Composite score (0.01.0).
22
+ score: Composite score (0.0-1.0).
20
23
  trust_score: Trust component of the score.
21
24
  recency_score: Recency component of the score.
22
25
  relevance_score: Relevance component of the score.
@@ -92,9 +95,7 @@ class TrustAwareRetriever:
92
95
  relevance = self._keyword_relevance(item.content, query_words)
93
96
 
94
97
  score = (
95
- self._w_trust * trust
96
- + self._w_recency * recency
97
- + self._w_relevance * relevance
98
+ self._w_trust * trust + self._w_recency * recency + self._w_relevance * relevance
98
99
  )
99
100
 
100
101
  if score >= min_score:
@@ -14,12 +14,14 @@ def _make_ledger(n: int, tokens_per_item: int = 100) -> ContextLedger:
14
14
  ledger = ContextLedger()
15
15
  for i in range(n):
16
16
  content = f"Item {i}: " + "x" * (tokens_per_item * 4)
17
- ledger.append(ContextItem(
18
- content=content,
19
- source="test",
20
- timestamp=time.time() - (n - i),
21
- token_estimate=tokens_per_item,
22
- ))
17
+ ledger.append(
18
+ ContextItem(
19
+ content=content,
20
+ source="test",
21
+ timestamp=time.time() - (n - i),
22
+ token_estimate=tokens_per_item,
23
+ )
24
+ )
23
25
  return ledger
24
26
 
25
27
 
@@ -54,18 +56,22 @@ def test_fifo_compress() -> None:
54
56
 
55
57
  def test_fifo_preserves_verified() -> None:
56
58
  ledger = ContextLedger()
57
- ledger.append(ContextItem(
58
- content="Verified fact",
59
- source="test",
60
- trust_level=TrustLevel.VERIFIED,
61
- token_estimate=100,
62
- ))
63
- for i in range(9):
64
- ledger.append(ContextItem(
65
- content=f"Inferred {i}",
59
+ ledger.append(
60
+ ContextItem(
61
+ content="Verified fact",
66
62
  source="test",
63
+ trust_level=TrustLevel.VERIFIED,
67
64
  token_estimate=100,
68
- ))
65
+ )
66
+ )
67
+ for i in range(9):
68
+ ledger.append(
69
+ ContextItem(
70
+ content=f"Inferred {i}",
71
+ source="test",
72
+ token_estimate=100,
73
+ )
74
+ )
69
75
 
70
76
  compressor = ContextCompressor(
71
77
  policy=CompressionPolicy.FIFO,
@@ -79,17 +85,21 @@ def test_fifo_preserves_verified() -> None:
79
85
 
80
86
  def test_semantic_lossless() -> None:
81
87
  ledger = ContextLedger()
82
- ledger.append(ContextItem(
83
- content="First sentence. Second sentence. Third sentence.",
84
- source="test",
85
- token_estimate=50,
86
- ))
87
- ledger.append(ContextItem(
88
- content="Verified content stays intact",
89
- source="test",
90
- trust_level=TrustLevel.VERIFIED,
91
- token_estimate=50,
92
- ))
88
+ ledger.append(
89
+ ContextItem(
90
+ content="First sentence. Second sentence. Third sentence.",
91
+ source="test",
92
+ token_estimate=50,
93
+ )
94
+ )
95
+ ledger.append(
96
+ ContextItem(
97
+ content="Verified content stays intact",
98
+ source="test",
99
+ trust_level=TrustLevel.VERIFIED,
100
+ token_estimate=50,
101
+ )
102
+ )
93
103
 
94
104
  compressor = ContextCompressor(
95
105
  policy=CompressionPolicy.SEMANTIC_LOSSLESS,
@@ -8,7 +8,9 @@ from pyagent_context.item import ContextItem, TrustLevel
8
8
  from pyagent_context.ledger import ContextLedger
9
9
 
10
10
 
11
- def _make_item(content: str, source: str = "agent", trust: TrustLevel = TrustLevel.INFERRED, age: float = 0.0) -> ContextItem:
11
+ def _make_item(
12
+ content: str, source: str = "agent", trust: TrustLevel = TrustLevel.INFERRED, age: float = 0.0
13
+ ) -> ContextItem:
12
14
  return ContextItem(
13
15
  content=content,
14
16
  source=source,
@@ -44,7 +46,7 @@ def test_query_by_trust() -> None:
44
46
  def test_query_by_age() -> None:
45
47
  ledger = ContextLedger()
46
48
  ledger.append(_make_item("old", age=7200)) # 2 hours ago
47
- ledger.append(_make_item("new", age=60)) # 1 minute ago
49
+ ledger.append(_make_item("new", age=60)) # 1 minute ago
48
50
 
49
51
  results = ledger.query(max_age_seconds=3600)
50
52
  assert len(results) == 1
@@ -26,19 +26,23 @@ def test_sweep_expired() -> None:
26
26
  def test_freshness_decay() -> None:
27
27
  ledger = ContextLedger()
28
28
  # Old item: 2 hours ago
29
- ledger.append(ContextItem(
30
- content="old item",
31
- source="s",
32
- timestamp=time.time() - 7200,
33
- token_estimate=100,
34
- ))
29
+ ledger.append(
30
+ ContextItem(
31
+ content="old item",
32
+ source="s",
33
+ timestamp=time.time() - 7200,
34
+ token_estimate=100,
35
+ )
36
+ )
35
37
  # New item: just now
36
- ledger.append(ContextItem(
37
- content="new item",
38
- source="s",
39
- timestamp=time.time(),
40
- token_estimate=100,
41
- ))
38
+ ledger.append(
39
+ ContextItem(
40
+ content="new item",
41
+ source="s",
42
+ timestamp=time.time(),
43
+ token_estimate=100,
44
+ )
45
+ )
42
46
 
43
47
  lifecycle = ContextLifecycle()
44
48
  decayed = lifecycle.apply_freshness_decay(ledger, half_life_seconds=3600)
@@ -53,21 +57,27 @@ def test_freshness_decay() -> None:
53
57
  def test_consolidation() -> None:
54
58
  ledger = ContextLedger()
55
59
  # Two similar items from same source
56
- ledger.append(ContextItem(
57
- content="Python asyncio patterns for concurrency",
58
- source="researcher",
59
- trust_level=TrustLevel.INFERRED,
60
- ))
61
- ledger.append(ContextItem(
62
- content="Python asyncio patterns for parallel tasks",
63
- source="researcher",
64
- trust_level=TrustLevel.VERIFIED,
65
- ))
60
+ ledger.append(
61
+ ContextItem(
62
+ content="Python asyncio patterns for concurrency",
63
+ source="researcher",
64
+ trust_level=TrustLevel.INFERRED,
65
+ )
66
+ )
67
+ ledger.append(
68
+ ContextItem(
69
+ content="Python asyncio patterns for parallel tasks",
70
+ source="researcher",
71
+ trust_level=TrustLevel.VERIFIED,
72
+ )
73
+ )
66
74
  # Different source
67
- ledger.append(ContextItem(
68
- content="JavaScript React hooks",
69
- source="frontend_agent",
70
- ))
75
+ ledger.append(
76
+ ContextItem(
77
+ content="JavaScript React hooks",
78
+ source="frontend_agent",
79
+ )
80
+ )
71
81
 
72
82
  lifecycle = ContextLifecycle(consolidation_threshold=0.4)
73
83
  consolidated = lifecycle.consolidate(ledger)
@@ -3,16 +3,15 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import tempfile
6
- from pathlib import Path
7
6
 
8
- from pyagent_context.item import ContextItem, TrustLevel
9
- from pyagent_context.memory.working import WorkingMemory
10
- from pyagent_context.memory.session import SessionMemory
7
+ from pyagent_context.item import ContextItem
11
8
  from pyagent_context.memory.semantic import InMemorySemanticStore
12
-
9
+ from pyagent_context.memory.session import SessionMemory
10
+ from pyagent_context.memory.working import WorkingMemory
13
11
 
14
12
  # ── WorkingMemory ─────────────────────────────────────────────────────
15
13
 
14
+
16
15
  def test_working_memory_add() -> None:
17
16
  wm = WorkingMemory(max_items=10, max_tokens=5000)
18
17
  item = ContextItem(content="hello", source="test")
@@ -48,6 +47,7 @@ def test_working_memory_utilization() -> None:
48
47
 
49
48
  # ── SessionMemory (JSON) ─────────────────────────────────────────────
50
49
 
50
+
51
51
  def test_session_memory_json_persist() -> None:
52
52
  with tempfile.TemporaryDirectory() as tmpdir:
53
53
  sm = SessionMemory("test-session", backend="json", storage_path=tmpdir)
@@ -73,6 +73,7 @@ def test_session_memory_json_clear() -> None:
73
73
 
74
74
  # ── SessionMemory (SQLite) ───────────────────────────────────────────
75
75
 
76
+
76
77
  def test_session_memory_sqlite_persist() -> None:
77
78
  with tempfile.TemporaryDirectory() as tmpdir:
78
79
  sm = SessionMemory("test-session", backend="sqlite", storage_path=tmpdir)
@@ -88,6 +89,7 @@ def test_session_memory_sqlite_persist() -> None:
88
89
 
89
90
  # ── InMemorySemanticStore ────────────────────────────────────────────
90
91
 
92
+
91
93
  def test_semantic_add_and_search() -> None:
92
94
  store = InMemorySemanticStore()
93
95
  store.add(ContextItem(content="Python asyncio event loop concurrency", source="docs"))
@@ -97,7 +99,9 @@ def test_semantic_add_and_search() -> None:
97
99
  results = store.search("Python async web")
98
100
  assert len(results) > 0
99
101
  # Python items should score higher
100
- assert "python" in results[0].item.content.lower() or "python" in results[0].item.content.lower()
102
+ assert (
103
+ "python" in results[0].item.content.lower() or "python" in results[0].item.content.lower()
104
+ )
101
105
 
102
106
 
103
107
  def test_semantic_remove() -> None:
@@ -27,18 +27,22 @@ def test_retrieval_ranks_by_relevance() -> None:
27
27
  def test_retrieval_ranks_by_trust() -> None:
28
28
  now = time.time()
29
29
  ledger = ContextLedger()
30
- ledger.append(ContextItem(
31
- content="Database connection pooling",
32
- source="s",
33
- trust_level=TrustLevel.INFERRED,
34
- timestamp=now,
35
- ))
36
- ledger.append(ContextItem(
37
- content="Database connection pooling",
38
- source="s",
39
- trust_level=TrustLevel.VERIFIED,
40
- timestamp=now,
41
- ))
30
+ ledger.append(
31
+ ContextItem(
32
+ content="Database connection pooling",
33
+ source="s",
34
+ trust_level=TrustLevel.INFERRED,
35
+ timestamp=now,
36
+ )
37
+ )
38
+ ledger.append(
39
+ ContextItem(
40
+ content="Database connection pooling",
41
+ source="s",
42
+ trust_level=TrustLevel.VERIFIED,
43
+ timestamp=now,
44
+ )
45
+ )
42
46
 
43
47
  retriever = TrustAwareRetriever(weight_trust=0.9, weight_relevance=0.05, weight_recency=0.05)
44
48
  results = retriever.retrieve(ledger, "database connection")
@@ -50,11 +54,13 @@ def test_retrieval_ranks_by_trust() -> None:
50
54
 
51
55
  def test_retrieval_excludes_expired() -> None:
52
56
  ledger = ContextLedger()
53
- ledger.append(ContextItem(
54
- content="expired item about Python",
55
- source="s",
56
- expires_at=time.time() - 10,
57
- ))
57
+ ledger.append(
58
+ ContextItem(
59
+ content="expired item about Python",
60
+ source="s",
61
+ expires_at=time.time() - 10,
62
+ )
63
+ )
58
64
  ledger.append(ContextItem(content="active item about Python", source="s"))
59
65
 
60
66
  retriever = TrustAwareRetriever()