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
2 changes: 2 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@
3. `make test` — test suite.
4. Check `BuildStats` fields to understand what the context engine dropped and why.
5. Use `ContextManager.artifact_store.list_refs()` to inspect intercepted tool outputs.
6. Enable `logging.DEBUG` on `contextweaver.context` to trace pipeline stages.
7. Enable `logging.DEBUG` on `contextweaver.routing` to trace beam search.

## Running Tests

Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ See [docs/agent-context/invariants.md](docs/agent-context/invariants.md) for the
3. `make test` — run the test suite.
4. Check `BuildStats` fields to understand what the context engine dropped and why.
5. Use `ContextManager.artifact_store.list_refs()` to inspect intercepted tool outputs.
6. Enable `logging.DEBUG` on `contextweaver.context` to trace pipeline stages (candidate counts, scores, drops, budget usage).
7. Enable `logging.DEBUG` on `contextweaver.routing` to trace beam search expansions and scoring.

## Adding a Feature

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Python `logging` integration with structured events across all subsystems (#111)
- Loggers: `contextweaver.context`, `contextweaver.routing`, `contextweaver.store`, `contextweaver.adapters`
- DEBUG-level messages at each context pipeline stage (candidate generation, scoring, dedup, selection, firewall, sensitivity)
- INFO-level summary messages for context builds and route completions
- Sensitivity guard: item text content is never logged at any level
- Path-scoped Copilot instructions for `context/` and `routing/` (#95)

## [0.1.5] - 2026-03-07
Expand Down
10 changes: 10 additions & 0 deletions src/contextweaver/adapters/a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any, Literal

from contextweaver.envelope import ResultEnvelope
from contextweaver.exceptions import CatalogError
from contextweaver.types import ArtifactRef, ContextItem, ItemKind, SelectableItem

logger = logging.getLogger("contextweaver.adapters")


def a2a_agent_to_selectable(agent_card: dict[str, Any]) -> SelectableItem:
"""Convert an A2A agent card dict to a :class:`SelectableItem`.
Expand Down Expand Up @@ -62,6 +65,7 @@ def a2a_agent_to_selectable(agent_card: dict[str, Any]) -> SelectableItem:
input_modes: list[str] = agent_card.get("defaultInputModes") or []
output_modes: list[str] = agent_card.get("defaultOutputModes") or []

logger.debug("a2a_agent_to_selectable: name=%s, skills=%d", name, len(skills))
return SelectableItem(
id=f"a2a:{name}",
kind="agent",
Expand Down Expand Up @@ -161,6 +165,12 @@ def a2a_result_to_envelope(
if ":" in stripped and len(stripped) < 200:
facts.append(stripped)

logger.debug(
"a2a_result_to_envelope: agent=%s, status=%s, artifacts=%d",
agent_name,
env_status,
len(artifact_refs),
)
return ResultEnvelope(
status=env_status,
summary=summary[:500] if len(summary) > 500 else summary,
Expand Down
11 changes: 11 additions & 0 deletions src/contextweaver/adapters/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any, Literal

from contextweaver.envelope import ResultEnvelope
from contextweaver.exceptions import CatalogError
from contextweaver.types import ArtifactRef, ContextItem, ItemKind, SelectableItem

logger = logging.getLogger("contextweaver.adapters")


def infer_namespace(tool_name: str) -> str:
"""Infer a namespace from an MCP tool name.
Expand Down Expand Up @@ -102,6 +105,7 @@ def mcp_tool_to_selectable(tool_def: dict[str, Any]) -> SelectableItem:
side_effects = not annotations.get("readOnlyHint", False)
cost_hint = float(annotations.get("costHint", 0.0))

logger.debug("mcp_tool_to_selectable: name=%s, tags=%s", name, sorted(tags))
return SelectableItem(
id=f"mcp:{name}",
kind="tool",
Expand Down Expand Up @@ -291,6 +295,13 @@ def mcp_result_to_envelope(
artifacts=artifacts,
provenance=provenance,
)
logger.debug(
"mcp_result_to_envelope: tool=%s, status=%s, artifacts=%d, facts=%d",
tool_name,
status,
len(artifacts),
len(envelope.facts),
)
return envelope, binaries, full_text


Expand Down
14 changes: 13 additions & 1 deletion src/contextweaver/context/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@

from __future__ import annotations

import logging

from contextweaver.config import ContextPolicy
from contextweaver.exceptions import ItemNotFoundError
from contextweaver.protocols import EventLog
from contextweaver.types import ContextItem, Phase

logger = logging.getLogger("contextweaver.context")


def generate_candidates(
event_log: EventLog,
Expand All @@ -32,7 +36,14 @@ def generate_candidates(
A list of :class:`~contextweaver.types.ContextItem` in log order.
"""
allowed = set(policy.allowed_kinds_per_phase.get(phase, []))
return [item for item in event_log.all() if item.kind in allowed]
result = [item for item in event_log.all() if item.kind in allowed]
logger.debug(
"generate_candidates: phase=%s, allowed_kinds=%s, candidates=%d",
phase.value,
sorted(k.value for k in allowed),
len(result),
)
return result


def resolve_dependency_closure(
Expand Down Expand Up @@ -73,4 +84,5 @@ def resolve_dependency_closure(
# Preserve log order by re-sorting via original indices
all_ids = {item.id: item for item in items + extra}
ordered = [item for item in event_log.all() if item.id in all_ids]
logger.debug("dependency_closure: added=%d parents", closures)
return ordered, closures
5 changes: 5 additions & 0 deletions src/contextweaver/context/dedup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@

from __future__ import annotations

import logging

from contextweaver._utils import jaccard, tokenize
from contextweaver.types import ContextItem

logger = logging.getLogger("contextweaver.context")

# FUTURE: merge compression to combine near-duplicate items instead of dropping.


Expand Down Expand Up @@ -50,4 +54,5 @@ def deduplicate_candidates(
else:
removed += 1

logger.debug("deduplicate_candidates: removed=%d, kept=%d", removed, len(kept))
return kept, removed
9 changes: 9 additions & 0 deletions src/contextweaver/context/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from __future__ import annotations

import logging
from typing import Literal

from contextweaver.context.views import ViewRegistry, generate_views
Expand All @@ -16,6 +17,8 @@
from contextweaver.summarize.extract import extract_facts
from contextweaver.types import ContextItem, ItemKind

logger = logging.getLogger("contextweaver.context")


def _default_summary(raw: str, max_chars: int = 500) -> str:
"""Return a truncated first-paragraph summary of *raw*."""
Expand Down Expand Up @@ -114,6 +117,7 @@ def apply_firewall(
)

_hook.on_firewall_triggered(item, "tool_result intercepted")
logger.debug("firewall: intercepted item_id=%s, summary_len=%d", item.id, len(summary))
return processed, envelope


Expand Down Expand Up @@ -147,4 +151,9 @@ def apply_firewall_to_batch(
processed.append(p)
if env is not None:
envelopes.append(env)
logger.debug(
"firewall_batch: processed=%d, intercepted=%d",
len(processed),
len(envelopes),
)
return processed, envelopes
22 changes: 22 additions & 0 deletions src/contextweaver/context/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any, Literal

from contextweaver.config import ContextBudget, ContextPolicy, ScoringConfig
Expand Down Expand Up @@ -52,6 +53,8 @@
from contextweaver.routing.catalog import Catalog
from contextweaver.routing.router import Router, RouteResult

logger = logging.getLogger("contextweaver.context")


class ContextManager:
"""Orchestrates the full context compilation pipeline.
Expand Down Expand Up @@ -146,6 +149,7 @@ def ingest(self, item: ContextItem) -> None:
item: The context item to ingest.
"""
self._event_log.append(item)
logger.debug("ingest: item_id=%s, kind=%s", item.id, item.kind.value)

def ingest_sync(self, item: ContextItem) -> None:
"""Synchronous alias for :meth:`ingest`."""
Expand Down Expand Up @@ -205,6 +209,11 @@ def ingest_tool_result(
# Shouldn't happen for tool_result items, but be safe
envelope = ResultEnvelope(status="ok", summary=raw_output[:500])
self._event_log.append(processed)
logger.debug(
"ingest_tool_result: item_id=%s, firewall=True, output_len=%d",
processed.id,
len(raw_output),
)
return processed, envelope

# Small output: extract facts and store in artifact store to enable drilldown
Expand Down Expand Up @@ -248,6 +257,11 @@ def ingest_tool_result(
artifact_ref=ref,
)
self._event_log.append(item)
logger.debug(
"ingest_tool_result: item_id=%s, firewall=False, output_len=%d",
item.id,
len(raw_output),
)
return item, envelope

def ingest_tool_result_sync(
Expand Down Expand Up @@ -620,6 +634,14 @@ def _build(

pack = ContextPack(prompt=prompt, stats=stats, phase=phase, envelopes=envelopes)
self._hook.on_context_built(pack)
logger.info(
"context build: phase=%s, included=%d, dropped=%d, tokens=%d/%d",
phase.value,
stats.included_count,
stats.dropped_count,
sum(stats.tokens_per_section.values()),
effective_budget.for_phase(phase),
)
return pack

async def build(
Expand Down
11 changes: 11 additions & 0 deletions src/contextweaver/context/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@

from __future__ import annotations

import logging

from contextweaver._utils import jaccard, tokenize
from contextweaver.config import ScoringConfig
from contextweaver.types import ContextItem, ItemKind

logger = logging.getLogger("contextweaver.context")

# Higher value → higher priority when included in context
_KIND_PRIORITY: dict[ItemKind, float] = {
ItemKind.policy: 1.0,
Expand Down Expand Up @@ -96,4 +100,11 @@ def score_candidates(
for i, item in enumerate(items)
]
scored.sort(key=lambda x: (-x[0], x[1].id))
if scored:
logger.debug(
"score_candidates: total=%d, top_score=%.4f, bottom_score=%.4f",
len(scored),
scored[0][0],
scored[-1][0],
)
return scored
12 changes: 12 additions & 0 deletions src/contextweaver/context/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@

from __future__ import annotations

import logging

from contextweaver.config import ContextBudget, ContextPolicy
from contextweaver.envelope import BuildStats
from contextweaver.protocols import TokenEstimator
from contextweaver.types import ContextItem, Phase

logger = logging.getLogger("contextweaver.context")


def select_and_pack(
scored: list[tuple[float, ContextItem]],
Expand Down Expand Up @@ -81,4 +85,12 @@ def select_and_pack(
dropped_count=dropped,
dropped_reasons=dropped_reasons,
)
logger.debug(
"select_and_pack: included=%d, dropped=%d, tokens=%d/%d, reasons=%s",
included,
dropped,
tokens_used,
token_limit,
dropped_reasons,
)
return selected, stats
10 changes: 10 additions & 0 deletions src/contextweaver/context/sensitivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@

from __future__ import annotations

import logging
from dataclasses import replace

from contextweaver.config import ContextPolicy
from contextweaver.protocols import RedactionHook
from contextweaver.types import ContextItem, Sensitivity

logger = logging.getLogger("contextweaver.context")

# Ordered severity levels for comparison.
_SENSITIVITY_ORDER: dict[Sensitivity, int] = {
Sensitivity.public: 0,
Expand Down Expand Up @@ -152,4 +155,11 @@ def apply_sensitivity_filter(
else:
result.append(item)

logger.debug(
"sensitivity_filter: action=%s, floor=%s, passed=%d, dropped=%d",
action,
policy.sensitivity_floor.value,
len(result),
dropped,
)
return result, dropped
11 changes: 11 additions & 0 deletions src/contextweaver/routing/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from typing import Any

Expand All @@ -14,6 +15,8 @@
from contextweaver.routing.graph import ChoiceGraph
from contextweaver.types import SelectableItem

logger = logging.getLogger("contextweaver.routing")

# ---------------------------------------------------------------------------
# RouteResult
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -295,6 +298,14 @@ def route(self, query: str, *, debug: bool = False) -> RouteResult:
if debug:
result.debug_trace = trace

if logger.isEnabledFor(logging.INFO):
logger.info(
"route: query_len=%d, top_k=%d, candidates=%d, scores=%s",
len(query),
self._top_k,
len(result.candidate_ids),
[round(s, 4) for s in result.scores[:5]],
)
return result

def _expand_subtree(
Expand Down
Loading