Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ It prepares context and routes tools but never calls models or executes tools.
| `store/async_protocols.py` | Async counterparts `AsyncEventLog` / `AsyncArtifactStore` / `AsyncEpisodicStore` / `AsyncFactStore` (issue #495) — same surface, `async def`. Consumed only by the async `context/` path; backend-agnostic. |
| `store/async_bridge.py` | `to_async(sync_store)` — wraps a *thread-safe* sync backend as the matching async protocol via `asyncio.to_thread`. Thread-affine backends (`SqliteEventLog`, `check_same_thread=True`) are not valid targets (issue #495). |
| `store/_async_to_sync.py` | Inverse bridges + `to_sync(async_store, loop)` + `is_async_store()` (issue #495). Drives async stores on a private `_LoopThread` so the existing sync pipeline can consume them; `ContextManager` offloads `build` to a worker thread when async-backed. Not public API. |
| `exceptions.py` | Custom exception hierarchy (all errors inherit `ContextWeaverError`) |
| `exceptions.py` | Custom exception hierarchy (all errors inherit `ContextWeaverError`). Each class carries a stable, frozen `code` (e.g. `CW_CONFIG`) plus an optional `hint`; `str(exc)` renders `[code] message (hint: …)`. Codes are documented in `docs/errors.md` and golden-listed in `tests/test_exceptions.py` (issues #635, #637). |
| `_utils.py` | Text similarity primitives: `tokenize()`, `jaccard()`, `TfIdfScorer` |
| `secrets.py` | Pure, deterministic secret detection/scrubbing primitives: `scrub_secrets()`, `scrub_secrets_in_list()`, `contains_secret()`, `SecretPattern` (issue #428). Shared by the firewall secret-scrub, the `SecretRedactor` hook, the sensitivity classifier, and ChoiceCard scrubbing. No I/O; never weakens a surface (only removes characters). |
| `_version.py` | Single-source version derived from `importlib.metadata`; fallback `"0.0.0+local"` |
Expand Down Expand Up @@ -239,7 +239,7 @@ These are strongly recommended. Engineering judgment applies — deviate with go

- **Text similarity in `_utils.py` only** — `tokenize()`, `jaccard()`, `TfIdfScorer` are the single source of truth. Do not duplicate.
- **`from __future__ import annotations`** in every source file.
- **All exceptions from `contextweaver.exceptions`** — use the custom hierarchy, not bare `ValueError`/`RuntimeError`.
- **All exceptions from `contextweaver.exceptions`** — use the custom hierarchy, not bare `ValueError`/`RuntimeError`. A new exception class needs a unique stable `code`, a `GOLDEN_CODES` entry in `tests/test_exceptions.py`, and a section in `docs/errors.md`.
- **`to_dict()` / `from_dict()` on all dataclasses** — complements `serde.py`; they are not redundant. See [invariants](docs/agent-context/invariants.md#serialization-design).
- **Deterministic by default** — tie-break by ID, sorted keys. No randomness in core pipelines.
- **No wildcard imports** — never use `from contextweaver import *`.
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Stable error codes + remediation hints (#635).** Every
`ContextWeaverError` subclass now carries a frozen, machine-readable `code`
(e.g. `CW_CONFIG`) so programs can branch on failures without string-matching,
plus an optional `hint` (with a class-level `default_hint` fallback). `str(exc)`
renders `[code] message (hint: …)`, so CLI error output surfaces both
automatically. Codes are golden-listed in `tests/test_exceptions.py` (a rename
or a code-less new exception fails CI).
- **Error reference page (#637).** New `docs/errors.md` documents every
exception — stable code, raising modules, common causes, and the fix — with a
code index table; added to the mkdocs nav, cross-linked from the
troubleshooting guide, and included in `llms.txt` / `llms-full.txt`.
- **Runtime deprecation machinery (#517).** New internal
`contextweaver._deprecation` module — `warn_deprecated(...)`, a `@deprecated`
decorator, and a single registry surfaced via `active_deprecations()` — emits
Expand Down
28 changes: 14 additions & 14 deletions api/public_api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
# Drift here means the public surface changed — review the diff deliberately.

## contextweaver
class ArtifactNotFoundError(ContextWeaverError)(...)
class ArtifactNotFoundError(ContextWeaverError)(*args: 'object', hint: 'str | None' = ...) -> 'None'
class ArtifactRef(handle: 'str', media_type: 'str', size_bytes: 'int', label: 'str' = ..., content_hash: 'str' = ...) -> None
def from_dict(cls, data: 'dict[str, Any]') -> 'ArtifactRef'
def to_dict(self) -> 'dict[str, Any]'
class ArtifactStoreQuotaError(ContextWeaverError)(...)
class ArtifactStoreQuotaError(ContextWeaverError)(*args: 'object', hint: 'str | None' = ...) -> 'None'
class BM25Scorer() -> 'None'
def fit(self, documents: 'list[str]') -> 'None'
def score(self, query: 'str', doc_index: 'int') -> 'float'
def score_all(self, query: 'str') -> 'list[float]'
class BeamSearchNavigator(*, beam_width: 'int' = ..., max_depth: 'int' = ..., top_k: 'int' = ..., confidence_gap: 'float' = ...) -> 'None'
def navigate(self, query: 'str', graph: 'ChoiceGraph', active_items: 'dict[str, SelectableItem]', scorer: 'Retriever', doc_id_to_idx: 'dict[str, int]', *, all_item_ids: 'set[str] | None' = ..., debug: 'bool' = ...) -> 'NavigationResult'
class BudgetExceededError(ContextWeaverError)(...)
class BudgetOverflowError(ContextWeaverError)(message: 'str', *, stats: 'BuildStats', dropped_kinds: 'list[str] | None' = ...) -> 'None'
class BudgetExceededError(ContextWeaverError)(*args: 'object', hint: 'str | None' = ...) -> 'None'
class BudgetOverflowError(ContextWeaverError)(message: 'str', *, stats: 'BuildStats', dropped_kinds: 'list[str] | None' = ..., hint: 'str | None' = ...) -> 'None'
class BuildStats(tokens_per_section: 'dict[str, int]' = ..., total_candidates: 'int' = ..., included_count: 'int' = ..., dropped_count: 'int' = ..., dropped_reasons: 'dict[str, int]' = ..., dropped_items: 'list[DroppedItem]' = ..., dedup_removed: 'int' = ..., dependency_closures: 'int' = ..., header_footer_tokens: 'int' = ..., token_estimator: 'str' = ..., firewall_events: 'list[FirewallStats]' = ...) -> None
def firewall_summary(self) -> 'FirewallStats'
def from_dict(cls, data: 'dict[str, Any]') -> 'BuildStats'
Expand All @@ -37,7 +37,7 @@
def to_dict(self) -> 'dict[str, Any]'
def validate_dependencies(self) -> 'list[str]'
def validate_references(self) -> 'CatalogValidationReport'
class CatalogError(ContextWeaverError)(...)
class CatalogError(ContextWeaverError)(*args: 'object', hint: 'str | None' = ...) -> 'None'
class CatalogNormalizer(*, strict: 'bool' = ..., lowercase_tags: 'bool' = ...) -> 'None'
def normalize(self, items: 'list[SelectableItem]') -> 'tuple[list[SelectableItem], NormalizationReport]'
class ChoiceCard(id: 'str', name: 'str', description: 'str', tags: 'list[str]' = ..., kind: "Literal['tool', 'agent', 'skill', 'internal', 'flow']" = ..., namespace: 'str' = ..., has_schema: 'bool' = ..., score: 'float | None' = ..., cost_hint: 'float' = ..., side_effects: 'bool' = ..., safety: "Literal['', 'read_only', 'destructive']" = ...) -> None
Expand All @@ -64,7 +64,7 @@
def cluster(self, items: 'list[SelectableItem]', *, k: 'int') -> 'dict[str, list[SelectableItem]]'
class CompactResult(firewalled: 'bool', payload: 'Any', summary: 'str | None', facts: 'list[str]' = ..., artifact_ref: 'str | None' = ..., stats: 'FirewallStats' = ...) -> None
def to_dict(self) -> 'dict[str, Any]'
class ConfigError(ContextWeaverError)(...)
class ConfigError(ContextWeaverError)(*args: 'object', hint: 'str | None' = ...) -> 'None'
class ContextBudget(route: 'int' = ..., call: 'int' = ..., interpret: 'int' = ..., answer: 'int' = ...) -> None
def for_phase(self, phase: 'Phase') -> 'int'
def from_dict(cls, data: 'dict[str, Any]') -> 'ContextBudget'
Expand All @@ -84,10 +84,10 @@
class ContextPolicy(allowed_kinds_per_phase: 'dict[Phase, list[ItemKind]]' = ..., max_items_per_kind: 'dict[ItemKind, int]' = ..., sensitivity_floor: 'Sensitivity' = ..., sensitivity_action: "Literal['drop', 'redact']" = ..., redaction_hooks: 'list[str]' = ..., allow_redacted_drilldown: 'bool' = ..., overflow_action: "Literal['drop', 'warn', 'raise']" = ..., overflow_raise_kinds: 'list[ItemKind] | None' = ..., extra: 'dict[str, Any]' = ...) -> None
def from_dict(cls, data: 'dict[str, Any]') -> 'ContextPolicy'
def to_dict(self) -> 'dict[str, Any]'
class ContextWeaverError(Exception)(...)
class ContextWeaverError(Exception)(*args: 'object', hint: 'str | None' = ...) -> 'None'
class DefaultCardPacker(*, max_cards: 'int' = ..., target_tokens_per_card: 'int | None' = ..., hard_cap_tokens_per_card: 'int | None' = ...) -> 'None'
def pack(self, items: 'list[SelectableItem]', scores: 'dict[str, float]', *, budget_tokens: 'int | None' = ...) -> 'list[ChoiceCard]'
class DeterminismError(ContextWeaverError)(...)
class DeterminismError(ContextWeaverError)(*args: 'object', hint: 'str | None' = ...) -> 'None'
class DeterministicScoreProvider()
def adjust(self, query: 'str', scored: 'list[tuple[str, float]]') -> 'list[tuple[str, float]]'
class DiagnosticEvent(event: 'str', timestamp: 'str' = ..., success: 'bool' = ..., duration_ms: 'float | None' = ..., session_id: 'str' = ..., tool_id: 'str | None' = ..., namespace: 'str | None' = ..., attributes: 'dict[str, Any]' = ..., version: 'int' = ...) -> None
Expand All @@ -98,7 +98,7 @@
class DroppedItem(item_id: 'str', reason: 'str') -> None
def from_dict(cls, data: 'dict[str, Any]') -> 'DroppedItem'
def to_dict(self) -> 'dict[str, str]'
class DuplicateItemError(ContextWeaverError)(...)
class DuplicateItemError(ContextWeaverError)(*args: 'object', hint: 'str | None' = ...) -> 'None'
EXPLANATION_VERSION: int
class EmbeddingBackend(Protocol)(*args, **kwargs)
def embed(self, texts: 'list[str]') -> 'list[list[float]]'
Expand Down Expand Up @@ -139,7 +139,7 @@
def from_dict(cls, data: 'dict[str, Any]') -> 'FirewallStats'
def to_dict(self) -> 'dict[str, Any]'
FuzzyScorer: NoneType
class GraphBuildError(ContextWeaverError)(message: 'str', *, cycle: 'list[str] | None' = ..., edge: 'tuple[str, str] | None' = ..., missing_root: 'str | None' = ...) -> 'None'
class GraphBuildError(ContextWeaverError)(message: 'str', *, cycle: 'list[str] | None' = ..., edge: 'tuple[str, str] | None' = ..., missing_root: 'str | None' = ..., hint: 'str | None' = ...) -> 'None'
class GraphManifest(manifest_version: 'int' = ..., build_hash: 'str' = ..., seed: 'int | None' = ..., engine_versions: 'dict[str, str]' = ..., timestamp: 'float' = ..., item_count: 'int' = ..., strategy: 'str' = ..., max_depth: 'int' = ..., extra: 'dict[str, Any]' = ...) -> None
def for_build(cls, items: 'list[SelectableItem]', *, strategy: 'str' = ..., max_depth: 'int' = ..., seed: 'int | None' = ..., engine_versions: 'dict[str, str] | None' = ..., extra: 'dict[str, Any] | None' = ..., timestamp: 'float | None' = ...) -> 'GraphManifest'
def from_dict(cls, data: 'dict[str, Any]') -> 'GraphManifest'
Expand Down Expand Up @@ -204,7 +204,7 @@
def to_dict(self) -> 'dict[str, Any]'
class ItemKind(str, Enum)
enum members: user_turn, agent_msg, tool_call, tool_result, doc_snippet, retrieved_doc, memory_fact, plan_state, policy
class ItemNotFoundError(ContextWeaverError)(...)
class ItemNotFoundError(ContextWeaverError)(*args: 'object', hint: 'str | None' = ...) -> 'None'
class JaccardClusteringEngine()
def cluster(self, items: 'list[SelectableItem]', *, k: 'int') -> 'dict[str, list[SelectableItem]]'
class JsonFileArtifactStore(base_dir: 'str | Path', *, max_bytes: 'int | None' = ..., max_artifacts: 'int | None' = ...) -> 'None'
Expand Down Expand Up @@ -263,7 +263,7 @@
PHASE_SCOPE_PREFERENCES: dict
class Phase(str, Enum)
enum members: route, call, interpret, answer
class PolicyViolationError(ContextWeaverError)(...)
class PolicyViolationError(ContextWeaverError)(*args: 'object', hint: 'str | None' = ...) -> 'None'
class ProfileConfig(mode: 'Mode' = ..., budget: 'ContextBudget' = ..., policy: 'ContextPolicy' = ..., scoring: 'ScoringConfig' = ..., routing: 'RoutingConfig' = ..., seed: 'int | None' = ...) -> None
def from_dict(cls, data: 'dict[str, Any]') -> 'ProfileConfig'
def from_preset(cls, name: 'str') -> 'ProfileConfig'
Expand All @@ -280,7 +280,7 @@
def fit(self, corpus: 'list[str]') -> 'None'
def score_one(self, query: 'str', index: 'int') -> 'float'
def search(self, query: 'str', top_k: 'int') -> 'list[tuple[int, float]]'
class RouteError(ContextWeaverError)(...)
class RouteError(ContextWeaverError)(*args: 'object', hint: 'str | None' = ...) -> 'None'
class RouteHistory(called_tool_ids: 'list[str]' = ..., last_result_summary: 'str | None' = ..., step_number: 'int' = ..., repeat_penalty: 'float' = ..., result_boost_weight: 'float' = ...) -> None
def from_dict(cls, data: 'dict[str, Any]') -> 'RouteHistory'
def to_dict(self) -> 'dict[str, Any]'
Expand Down Expand Up @@ -348,7 +348,7 @@
class StoreBundle(artifact_store: 'ArtifactStore | None' = ..., event_log: 'EventLog | None' = ..., episodic_store: 'EpisodicStore | None' = ..., fact_store: 'FactStore | None' = ...) -> None
def from_dict(cls, data: 'dict[str, Any]') -> 'StoreBundle'
def to_dict(self) -> 'dict[str, Any]'
class StoreClosedError(ContextWeaverError)(...)
class StoreClosedError(ContextWeaverError)(*args: 'object', hint: 'str | None' = ...) -> 'None'
class StructuredExtractor(max_chars: 'int' = ...) -> 'None'
def extract(self, raw: 'str', metadata: 'dict[str, Any]') -> 'list[str]'
class StructuredFirewall(keep: 'list[str]' = ..., max_fact_chars: 'int' = ...) -> None
Expand Down
Loading
Loading