Imported from demml/scouter (
AGENTS.md). Install upstream withnpx skills add demml/scouter. Copyright stays with the author.
AGENTS.md
Guidance for AI agents (Claude Code, Codex, Gemini CLI, etc.) working with this repository.
Project Overview
Scouter is a developer-first monitoring and observability toolkit for AAI workflows. It provides:
- Drift detection (PSI, SPC, custom metrics) for data and model monitoring
- Distributed tracing with OpenTelemetry-compatible span ingestion
- Agentic evaluation — both online (production sampling) and offline (batch)
- Alerting via Slack, OpsGenie, or Console, driven by scheduled background workers
The architecture is a Rust server + Python client. The Python package (scouter-ml) wraps Rust logic via PyO3. The server uses PostgreSQL for recent data and DataFusion for archival.
Verification After Any Code Change
After implementing any change, Claude must run the full verification sequence below before considering the task complete. Do not skip steps.
# 1. Rust — workspace-wide clippy (run from repo root)
make lints
# 2. Rust — unit tests for the changed crate
cargo test -p scouter-<crate> --all-features -- --nocapture --test-threads=1
# 3. Python — rebuild extension after any Rust change (run from py-scouter/)
make setup.project
# 4. Python — lints (ruff + pylint + mypy, run from py-scouter/)
make lints
# 5. Python — unit tests for changed area
uv run pytest tests/path/to/affected_test.py -s -v
Rules:
make lintsat repo root =cargo clippy --workspace --all-targets --all-features -- -D warningsmake lintsinsidepy-scouter/= ruff + pylint + mypy- Always run
make setup.project(frompy-scouter/) before Python lints/tests when Rust code changed - Clippy lints apply to test code too (
--all-targets) — use struct update syntax (..Default::default()) not field-by-field assignment afterDefault::default()
Commands
Rust (root)
cargo fmt --all # Format
cargo clippy --workspace --all-targets --all-features -- -D warnings # Lint (workspace-wide, use with care)
make test.unit # Unit tests (types, dispatch, drift, profile) — no Docker needed
make test.needs_sql # SQL + server + eval + drift executor tests (requires Docker)
make test.sql # PostgreSQL integration tests only
make test.server # Server integration tests only
make test.dataframe # DataFusion query tests only
make test.drift.executor # Drift executor background worker tests
# Run a single Rust test
cargo test -p scouter-<crate> <test_name> --all-features -- --nocapture --test-threads=1
# Start local server (spins up all Docker backends first)
make start.server
Python (py-scouter/)
make setup.project # Build Rust extension (maturin) + sync Python deps — run after Rust changes
make format # isort + black + ruff
make lints # ruff + pylint + mypy
make test.unit # pytest unit tests (excludes integration, benchmarks skipped)
make test.integration # All integration tests (requires running server)
make test.integration.api # FastAPI integration tests only
make test.integration.queue # Queue integration tests only
make test.integration.client # Client integration tests only
# Run a single Python test
cd py-scouter && uv run pytest tests/path/to/test_file.py::test_name -s
Docker
make build.all_backends # Start Postgres + Kafka + Redis + RabbitMQ (needed for server tests)
make build.sql # Start PostgreSQL only
make build.kafka # Start Kafka + Zookeeper only
make build.shutdown # Stop all services and remove volumes
Architecture
Repository Layout
scouter/
├── crates/ # Rust workspace crates
├── py-scouter/
│ ├── python/scouter/ # Python API (pure Python + thin wrappers over Rust types)
│ └── src/ # PyO3 FFI bindings (lib.rs registers all modules)
├── docker-compose.yml # Local dev services
└── makefile # Root build targets
Key Rust Crates
| Crate | Purpose |
|---|---|
scouter-server |
HTTP (Axum) + gRPC (Tonic) server; API routes in src/api/ |
scouter-sql |
PostgreSQL (sqlx), migrations, background workers (drift executor, Agent Poller) |
scouter-drift |
PSI, SPC, custom metric drift algorithms + binning strategies |
scouter-profile |
Data profiling (feature statistics, distributions) |
scouter-evaluate |
Agent eval: LLM judge tasks, assertion tasks, agent assertion tasks, comparison operators |
scouter-types |
Shared types/contracts across all crates |
scouter-events |
Kafka, RabbitMQ, Redis event bus adapters (feature-gated) |
scouter-tonic |
gRPC proto definitions |
scouter-tracing |
OpenTelemetry span ingestion and querying |
scouter-auth |
JWT authentication |
scouter-settings |
Server configuration (env vars → typed config) |
scouter-dispatch |
Alert dispatch (Slack, OpsGenie) |
scouter-observability |
Prometheus metrics setup |
Python Package Structure (py-scouter/python/scouter/)
| Module | Key Classes |
|---|---|
client/ |
ScouterClient — register profiles, query drift results |
drift/ |
Drifter — create PsiDriftProfile, SpcDriftProfile, CustomMetricDriftProfile |
profile/ |
DataProfiler — create DataProfile with feature statistics |
evaluate/ |
GenAIEvalProfile, EvalDataset, EvalRecord, AgentAssertionTask, AgentAssertion, execute_agent_assertion_tasks |
queue/ |
ScouterQueue — real-time record insertion (<1µs, non-blocking) |
tracing/ |
ScouterInstrumentor, get_tracer, TraceContext, SpanContext |
agent/ |
Agent provider integrations (Anthropic, Google) |
transport/ |
HttpTransportConfig, KafkaTransportConfig, RabbitMQTransportConfig |
alert/ |
SlackDispatchConfig, OpsGenieDispatchConfig, ConsoleDispatchConfig |
types/ |
Features, Metrics, CustomMetric, AlertThreshold |
PyO3 bindings in py-scouter/src/lib.rs register Rust modules into the Python extension. After changing Rust code, run make setup.project to rebuild. Stubs are generated via make build.stubs.
Server Architecture
The server runs as a dual-protocol service with shared Arc<AppState>:
- HTTP (Axum): REST API for client requests
- gRPC (Tonic): High-performance transport (used by
ScouterQueue)
AppState holds: PostgreSQL pool, task manager, auth manager, event consumer channels.
Background workers (in scouter-sql):
- Drift Executor: Scheduled via
pg_cron; queries stored drift profiles, computes drift against recent data, triggers alerts - Agent Poller: Picks up pending
EvalRecordbatches, runs evaluation tasks, checks alert conditions
Event Bus
Queue transport is feature-gated. The server binary is built with --all-features in CI/dev. Transport options: HTTP (default), Kafka (--features kafka), RabbitMQ (--features rabbitmq), Redis (--features redis_events).
Domain Model
Drift Profiles
Each profile type has a config + per-feature profile:
-
PSI (
PsiDriftProfile) — Bins baseline data into deciles; computes PSI = Σ(y_i - y_b) × ln(y_i / y_b). Thresholds: <0.1 stable, 0.1–0.25 moderate, >0.25 significant. Threshold types:PsiFixedThreshold,PsiNormalThreshold,PsiChiSquareThreshold. -
SPC (
SpcDriftProfile) — Calculates grand mean + stddev from baseline; establishes 3 control limit zones (1σ, 2σ, 3σ). Uses WECO rules (default "8 16 4 8 2 4 1 1") to detect out-of-control patterns. -
Custom (
CustomMetricDriftProfile) — User-defined named metrics withAlertThreshold(Below, Above, Outside).sample_size(default 25) controls aggregation before threshold check.
Agent Evaluation
Two evaluation modes:
Online (AgentEvalProfile): Real-time monitoring. EvalRecord objects are inserted into ScouterQueue during inference. Server samples based on sample_ratio, runs evaluation tasks asynchronously, checks alert conditions on schedule.
Offline (EvalDataset): Batch evaluation. Records → Tasks → Dataset → Execute → Review results.
Task types:
AssertionTask— Deterministic checks using 50+ComparisonOperatorvalues (Equals, GreaterThan, Contains, Matches, IsJson, etc.). Supportscontext_path(dot-notation field extraction) and template variable substitution (${field_name}). Setcondition=Trueto act as a conditional gate (skips downstream tasks on failure).LLMJudgeTask— LLM-powered semantic evaluation. Injects context variables intoPrompt. Supports OpenAI, Anthropic, Google. Uses structured output (Pydantic models).context_pathextracts field from LLM response. Multimodal judge tasks support static media baked into the prompt via potatoheadMediaRefand dynamic per-record media viaEvalRecord(media=[ImageMedia(id="chart", url=...)]), referenced with isolated${media:chart}placeholders. Sources are URL (provider fetches), rawbytes, or localpathread at construction; this is distinct from${var}text substitution. Prompt retry-on-schema-mismatch is not active for multimodal judge tasks in v1.AgentAssertionTask— Deterministic assertions on agentic LLM responses. Backed byAgentAssertion(14 variants):ToolCalled,ToolNotCalled,ToolCalledWithArgs(partial match),ToolCallSequence(ordered),ToolCallCount,ToolArgument,ToolResult,ResponseContent,ResponseModel,ResponseFinishReason,ResponseInputTokens,ResponseOutputTokens,ResponseTotalTokens,ResponseField(dot-notation escape hatch into raw JSON).AgentContextBuilderauto-detects vendor format from response JSON (choices→ OpenAI,stop_reason+content→ Anthropic,candidates→ Gemini/Vertex). Supports aresponsesub-key wrapper.ResponseFieldpaths are resolved against the response value (not the full context root). Useexecute_agent_assertion_tasks()for standalone evaluation or include inEvalDatasetalongside other task types.TraceAssertionTask— Assertions on OpenTelemetry spans fetched from Delta Lake. Used for tracing-aware evaluation in the online poller path.
Tasks can declare depends_on: ["task_id"] to access upstream outputs. Each task sees base context + declared dependencies only.
Alert Dispatch
Alert configs attach to drift/eval profiles and run on a cron schedule:
SlackDispatchConfig(channel, token)OpsGenieDispatchConfig(team, api_key)ConsoleDispatchConfigCommonCronsenum:EveryHour,Every6Hours,EveryDay, etc.
Data Types
Scouter accepts: Pandas DataFrames, NumPy 2D arrays, Polars DataFrames, Pydantic models.
Key Conventions
- Rust errors:
thiserrorfor error types; propagate with? - Async: Tokio multi-threaded throughout
- Python deps: Managed with
uv; useuv runfor all Python commands inpy-scouter/ - SQL tests: Must use
--test-threads=1for isolation - Clippy:
-D warnings— all warnings are errors in CI - Profile versioning: Profiles are identified by
(space, name, version)triple — used throughout the SQL schema and API routing - Feature gates: New event bus integrations go in
scouter-eventswith a Cargo feature flag
Server Environment Variables
| Variable | Default | Description |
|---|---|---|
DATABASE_URI |
required | PostgreSQL connection string |
MAX_POOL_SIZE |
— | DB connection pool size |
DATA_RETENTION_PERIOD |
30 days | Auto-purge threshold |
POLLING_WORKER_COUNT |
4 | Background job workers |
KAFKA_BROKERS |
— | Kafka bootstrap servers |
RABBITMQ_ADDR |
— | RabbitMQ AMQP URL |
REDIS_ADDR |
— | Redis URL |
SCOUTER_STORAGE_URI |
./scouter_storage |
Object storage (S3, GCS, Azure, local) |
SCOUTER_TRACE_REFRESH_INTERVAL_SECS |
10 |
How often each pod refreshes its Delta table snapshot from shared storage. Set lower (e.g. 5) for faster cross-pod visibility; set higher to reduce object-store LIST calls. Only relevant in multi-pod deployments. |
SCOUTER_TRACE_VISIBILITY_BUFFER_SECS |
refresh + 2 | Minimum delay before trace-backed evals are polled. Startup panics if this is lower than SCOUTER_TRACE_REFRESH_INTERVAL_SECS + 2, because a smaller buffer can make the poller fetch before a pod sees the committed anchor span. |
SCOUTER_INBOX_RECONCILE_AFTER_SECS |
15 |
Age floor before reconciliation scans awaiting_trace rows for missing anchor queue events. |
SCOUTER_INBOX_RECONCILE_LOOKBACK_SECS |
86400 |
Maximum supported anchor span start lookback used when reconciliation queries Delta for anchor spans. Increase for evals attached to spans that can run longer than 24 hours. |
SCOUTER_INBOX_RECONCILE_INTERVAL_SECS |
60 |
How often reconciliation recovers dropped live inbox notifications from Delta. |
SCOUTER_INBOX_RECONCILE_BATCH |
200 |
Maximum awaiting_trace rows scanned per reconciliation tick. |
SCOUTER_ENCRYPT_SECRET |
— | HMAC-SHA256 key (32 bytes) |
SCOUTER_BOOTSTRAP_KEY |
— | Initial admin bootstrap key |