{"slug": "show-hn-traccia-vendor-neutral-runtime-control-for-ai-agents", "title": "Show HN: Traccia – Vendor-neutral runtime control for AI agents", "summary": "Traccia, a vendor-neutral Python SDK for runtime control of AI agents, is now available on PyPI, offering observability, evaluation and policy enforcement built on OpenTelemetry. The SDK's @govern decorator can deny or reshape LLM and tool calls at runtime using policies including Spend Cap, Model Boundary and Loop Cap, and it auto-instruments OpenAI, Anthropic and Gemini (google-genai) plus requests and HTTP libraries, with integrations for LangChain, CrewAI and the OpenAI Agents SDK. Traccia also ships offline evaluation via evaluate(), prompt management with load_prompt and prefetch_prompts, and EU AI Act and HIPAA-oriented governance evidence with PII/PHI redaction helpers.", "body_md": "**AI agents can act. Traccia controls what they're allowed to do.**\n\nTraccia is a production-ready Python SDK for **observing, evaluating, and enforcing policies on AI agents and LLM applications at runtime.**\n\nBuilt on OpenTelemetry, Traccia provides distributed tracing, token and cost tracking, evaluation, governance evidence, and runtime policy enforcement across AI agents, agentic workflows, and multi-agent systems.\n\nFramework-agnostic and built for **OpenAI Agents, LangGraph, CrewAI, and LLM applications in production.**\n\n[Traccia](https://pypi.org/project/traccia/) is available on PyPI.\n\n- **Runtime Policies** :`@govern` can deny or reshape LLM or tool calls at runtime using policies such as Spend Cap, Model Boundary, and Loop Cap\n- **Guardrail Detection** : Passive detection of AI safety controls, provider-native safeguards, and custom guardrails\n- **Security Controls** : No secrets in logs and configurable data truncation\n\n- **Automatic Instrumentation** : Auto-patch OpenAI, Anthropic, Gemini (google-genai), requests, and HTTP libraries\n- **Framework Integrations** : Support for LangChain, CrewAI, and OpenAI Agents SDK\n- **LLM-Aware Tracing** : Track tokens, costs, prompts, completions, and latency automatically\n- **OpenTelemetry Metrics** : Emit OTEL-compliant metrics for accurate token and cost tracking independent of sampling\n- **Decorator-Based Tracing** : Trace any function with the`@observe` decorator\n- **Multiple Exporters** : OTLP-compatible export to Grafana Tempo, Jaeger, Zipkin, SigNoz, Console, or File\n- **Production-Ready Architecture** : Rate limiting, error handling, configuration validation, and reliable flushing\n- **High Performance** : Efficient batching, async support, and low-overhead instrumentation\n\n- **Offline Evaluation** :`evaluate()` runs a task and scorers over a dataset and saves an experiment you can open, compare, and attach on promote\n- **Prompt Management** :`load_prompt` /`prefetch_prompts` with cache, stale-while-revalidate, fallback, and`traccia.prompt.*` span identity\n- **Zero-Configuration Setup** : Simple`init()` call with automatic configuration discovery\n- **Type-Safe Configuration** : Full Pydantic validation and configuration management\n\n- **AI Governance and Compliance** : EU AI Act and HIPAA-oriented evidence, transparency records, integrity verification, and PII/PHI redaction helpers\n\n```\npip install traccia\npython\nfrom traccia import init, observe\n\n# Initialize (auto-loads from traccia.toml if present)\ninit()\n\n# Trace any function\n@observe()\ndef my_function(x, y):\n    return x + y\n\n# That's it! Traces are automatically created and exported\nresult = my_function(2, 3)\npython\nfrom traccia import init, observe\nfrom openai import OpenAI\n\ninit()  # Auto-patches OpenAI\n\nclient = OpenAI()\n\n@observe(as_type=\"llm\")\ndef generate_text(prompt: str) -> str:\n    response = client.chat.completions.create(\n        model=\"gpt-4\",\n        messages=[{\"role\": \"user\", \"content\": prompt}]\n    )\n    return response.choices[0].message.content\n\n# Automatically tracks: model, tokens, cost, prompt, completion, latency\ntext = generate_text(\"Write a haiku about Python\")\npython\nfrom traccia import init\nfrom google import genai\n\ninit()  # Auto-patches google-genai\n\nclient = genai.Client()\nresponse = client.interactions.create(\n    model=\"models/gemini-flash-latest\",\n    input=\"Write a haiku about Python\",\n)\n```\n\nFetch prompts from your Traccia prompt registry by name and deploy label. Compiling fills `{{variables}}` and stamps the active span with prompt identity.\n\n``` python\nfrom traccia import init, observe, load_prompt, prefetch_prompts\n\ninit(api_key=\"...\", prompt_cache_ttl_s=60)\nprefetch_prompts([\"support-reply\"])  # optional warm-up\n\n@observe(as_type=\"llm\")\ndef reply(question: str) -> str:\n    prompt = load_prompt(\n        \"support-reply\",\n        label=\"production\",\n        fallback={\n            \"type\": \"chat\",\n            \"messages\": [{\"role\": \"system\", \"content\": \"You are a helpful assistant.\"}],\n        },\n    )\n    messages = prompt.compile(question=question)\n    # pass messages to your LLM client\n    return messages[-1][\"content\"]\n```\n\nRequires the [Traccia platform](https://app.traccia.ai): create a workspace API key under **Settings → API Keys**. Tracing and prompt fetch use the same key and default to `https://api.traccia.ai`. Pass an explicit `fallback` so agents can still run if a fetch fails. See [Prompts in the SDK](https://traccia.ai/docs/sdk/prompts).\n\n`evaluate()` runs a task and scorers over a dataset. By default the run is saved as an experiment you can open in the app under **Evaluate → Experiments**, compare against other runs, and attach when you promote a prompt. Use the same workspace API key as tracing and `load_prompt`.\n\n``` python\nfrom traccia import init, evaluate, load_prompt\n\ninit(api_key=\"...\")\n\nprompt = load_prompt(\"support-reply\", label=\"production\")\n\ndef task(inp):\n    messages = prompt.compile(**inp)\n    return call_model(messages)\n\nresult = evaluate(\n    \"support-reply-v3\",\n    data=\"support-golden\",     # platform dataset name or UUID\n    task=task,\n    scorers=[\"exact_match\"],   # builtins, platform names/ids, or callables\n    prompt=\"support-reply\",    # labels the experiment cell with this prompt\n)\nprint(result.summary())\nprint(result.url)\n```\n\n- **Persist is on by default.** Set`persist=False` for a local-only loop (no experiment URL).\n- **Inline rows** work as`[{\"input\": {...}, \"expected\": \"...\"}]` . With persist on, Traccia still saves a full experiment (helper datasets named`sdk-eval/...` are hidden on**Evaluate → Datasets** unless you turn on**Show SDK-Created** ).\n- **Builtins** run in-process:`exact_match` ,`contains` ,`json_valid` . Mix in platform scorers (LLM-as-judge, Jev Decision, code) by name or UUID. Pass`provider_keys=` for judges (`typesafe` for Jev). A Jev score keeps each question on the experiment.\n- **One throwing row does not abort the run.** That cell records`error` ; other items still score. Configuration and API failures raise`EvaluateError` .\n- If the first task argument is named `row` ,`item` ,`example` , or`case` , Python passes the full row. Any other name (including`input` ) receives only the input dict.\n\nSee [Evaluate in the SDK](https://traccia.ai/docs/sdk/evaluate) and the walkthrough [Run Experiments From Code](https://traccia.ai/docs/guides/sdk-evaluate).\n\n`traccia eval` runs a task script, saves an experiment, and writes `summary.json`. The process exit code is the gate: 0 pass, 1 quality miss, 2 bad command or baseline, 3 untrustworthy evidence, 4 the experiment did not save, 5 the summary file was not written. With no pass-rate bar, a green check is **Execution Only**. Add `--min-pass-rate` or `--max-drop` for a **Quality Gate**. `--max-drop 0.05` is five percentage points. `--max-task-cost-increase 0.2` is 20 percent.\n\nThe script defines `task` and, for a function in the file, a `scorers` dict passed as `--scorer local:name`. A local scorer cannot pass `--max-drop`. Pass `--prompt` with `--prompt-version` or `--prompt-label` when the experiment should record a prompt version. Platform LLM-judge and Jev scorers read `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` or `GOOGLE_API_KEY`, `GROQ_API_KEY`, and `TYPESAFE_API_KEY`, or a JSON object in `TRACCIA_PROVIDER_KEYS`. Those values are not written into the summary. Each Jev question is gated on its own pass rate. Execution Only means no pass-rate gate; cost and latency gates can still apply.\n\n```\ntraccia eval experiments/support_agent.py \\\n  --output summary.json \\\n  --dataset support-golden \\\n  --scorer exact_match \\\n  --min-pass-rate 0.9 \\\n  --baseline-prompt support-reply \\\n  --max-drop 0.05\n```\n\nSee [Run Evals In CI](https://traccia.ai/docs/guides/ci-evals).\n\nCreate a callback handler and pass it to `config={\"callbacks\": [traccia_handler]}`. Install the optional extra: `pip install traccia[langchain]`.\n\n``` python\nfrom traccia import init\nfrom traccia.integrations.langchain import CallbackHandler  # or TracciaCallbackHandler\nfrom langchain_openai import ChatOpenAI\n\ninit()\n\n# Create Traccia handler (no args)\ntraccia_handler = CallbackHandler()\n\n# Use with any LangChain runnable\nllm = ChatOpenAI(model=\"gpt-4o-mini\")\nresult = llm.invoke(\n    \"Tell me a joke\",\n    config={\"callbacks\": [traccia_handler]}\n)\n```\n\nSpans for LLM/chat model runs are created automatically with the same attributes as direct OpenAI instrumentation (model, prompt, usage, cost).\n\n**Note:** `pip install traccia[langchain]` installs traccia plus `langchain-core`; you need this extra to use the callback handler. If you already have `langchain-core` (e.g. from `langchain` or `langchain-openai`), base `pip install traccia` may be enough at runtime, but `traccia[langchain]` is the supported way to get a compatible dependency.\n\nTraccia **automatically** detects and instruments the OpenAI Agents SDK when installed. No extra code needed:\n\n``` python\nfrom traccia import init\nfrom agents import Agent, Runner\n\ninit()  # Automatically enables Agents SDK tracing\n\nagent = Agent(\n    name=\"Assistant\",\n    instructions=\"You are a helpful assistant\"\n)\nresult = Runner.run_sync(agent, \"Write a haiku about recursion\")\n```\n\n**Configuration**: Auto-enabled by default when `openai-agents` is installed. To disable:\n\n```\ninit(openai_agents=False)  # Explicit parameter\n# OR set environment variable: TRACCIA_OPENAI_AGENTS=false\n# OR in traccia.toml under [instrumentation]: openai_agents = false\n```\n\n**Compatibility**: If you have `openai-agents` installed but don't use it (e.g., using LangChain or pure OpenAI instead), the integration is registered but never invoked—no overhead or extra spans.\n\nTraccia **automatically** instruments [CrewAI](https://docs.crewai.com/) when it is installed in your environment.\n\n``` python\nfrom traccia import init\nfrom crewai import Agent, Task, Crew, Process\n\ninit()  # Auto-enables CrewAI tracing when CrewAI is installed\n\nresearcher = Agent(role=\"Research Analyst\", goal=\"Research a topic\", llm=\"gpt-4o-mini\")\ntask = Task(description=\"Research Shawn Michaels\", agent=researcher)\n\ncrew = Crew(agents=[researcher], tasks=[task], process=Process.sequential, verbose=True)\nresult = crew.kickoff()\n```\n\nTraccia will create spans for the crew (`crewai.crew.kickoff`), each task (` crewai.task.*`), agents (` crewai.agent.*`), and underlying LLM calls, which nest under the existing OpenAI spans.\n\n**Configuration**: Auto-enabled by default when `crewai` is installed. To disable:\n\n```\ninit(crewai=False)  # Explicit parameter\n# OR set environment variable: TRACCIA_CREWAI=false\n# OR in traccia.toml under [instrumentation]: crewai = false\n```\n\nTraccia includes a passive guardrail detection engine that runs as a span processor — no runtime enforcement, no changes required to existing agent code. It inspects every span as it ends, classifies guardrail signals into structured findings, and writes results back onto spans so they appear in any configured exporter.\n\nDetection is automatic once `traccia.init()` is called. The processor runs three tiers against every span's attributes:\n\n| Tier | Source | Confidence | How | \n|---|---|---|---|\n| A — Explicit | `explicit` | `high` | `@observe(as_type=\"guardrail\")` or`guardrail_span()` | \n| B — Provider-native | `provider_native` | `high` /`medium` | LLM finish reason, stop reason, safety ratings | \n| C — Heuristic | `heuristic` | always `low` | Denial keywords in tool error messages | \n\nAt the end of each trace (when the root span ends), the processor evaluates which guardrail categories should exist given the agent's observed capabilities (LLM calls, tool use, user-provided text) and reports which are missing.\n\n**Option 1: `guardrail_span` context manager** — recommended for inline checks\n\n``` python\nfrom traccia.guardrails import guardrail_span\n\nwith guardrail_span(\"pii_check\", category=\"pii\", enforcement_mode=\"warn\") as span:\n    result = run_pii_check(user_input)\n    span.set_attribute(\"guardrail.triggered\", result.found_pii)\n```\n\n**Option 2: `@observe(as_type=\"guardrail\")` decorator** — for function-level guardrails\n\nIf your guardrail function returns a `bool`, `guardrail.triggered` is set automatically:\n\n``` python\nfrom traccia import observe\n\n@observe(\n    as_type=\"guardrail\",\n    attributes={\n        \"guardrail.name\": \"prompt_injection_check\",\n        \"guardrail.category\": \"prompt_injection\",\n        \"guardrail.enforcement_mode\": \"block\",\n    }\n)\ndef check_injection(text: str) -> bool:\n    return any(kw in text.lower() for kw in INJECTION_KEYWORDS)\n    # True → triggered (blocked), False → not triggered\n```\n\nFor non-bool returns, set `guardrail.triggered` manually on the current span.\n\nBatch pipelines or internal-only agents often get flagged for `prompt_injection` / `input_validation` because they make LLM calls with `llm.prompt`. Suppress specific categories for a run:\n\n``` python\nfrom traccia.guardrails import guardrail_span\n\n# Convenience: suppress_missing on guardrail_span\nwith guardrail_span(\"root\", category=\"unknown\", suppress_missing=[\"prompt_injection\", \"input_validation\"]):\n    run_batch_pipeline()\n\n# Or directly on any span\nfrom traccia.guardrails import ATTR_GUARDRAIL_SUPPRESS_MISSING\nspan.set_attribute(ATTR_GUARDRAIL_SUPPRESS_MISSING, [\"prompt_injection\"])\n```\n\n**Per span** (when a guardrail signal is found):\n\n- `guardrail.finding.count` — number of findings on this span\n- `guardrail.findings` — JSON array of`GuardrailFinding` objects\n\n**On the root span** (aggregated summary of the entire run):\n\n- `guardrail.summary` — full`GuardrailSummary` JSON\n- `guardrail.summary.coverage_confidence` —`\"high\"` ,`\"medium\"` , or`\"low\"`\n- `guardrail.summary.missing_count` — number of expected-but-missing guardrail categories\n- `guardrail.summary.detected_categories` — list of detected category strings\n\nThese signals are detected automatically from LLM span attributes — no annotation required:\n\n| Provider | Signal | Attribute | \n|---|---|---|\n| OpenAI | `finish_reason = \"content_filter\"` | `llm.finish_reason` | \n| Azure OpenAI | `finish_reason = \"content_filtered\"` | `llm.finish_reason` | \n| Google GenAI | `finish_reason = \"SAFETY\"` | `llm.finish_reason` | \n| Anthropic | `stop_reason = \"content_filter\"` | `llm.stop_reason` | \n| Anthropic | Policy violation error message | `error.message` +`llm.vendor=anthropic` | \n| Google/LangChain | `\"blocked\": true` or`\"probability\": \"HIGH\"` in safety ratings | `llm.safety_ratings` | \n\nTier A/B stay on. Turn off tool-error keyword heuristics if you do not want Tier C at all:\n\n```\ninit(guardrail_heuristics=False)\n```\n\nOr `TRACCIA_GUARDRAIL_HEURISTICS=false`, or in `traccia.toml`: `[instrumentation]` → `guardrail_heuristics = false`.\n\n- Guardrails running outside the traced process (API gateways, proxies, external validators) are invisible unless they write span attributes.\n- A guardrail that exists but never fires cannot be distinguished from a missing guardrail. Only explicit annotation proves presence.\n- Prompt injection detection requires an explicit span — the model's output alone cannot reliably indicate whether a check ran.\n- Capability inference (`handles_user_text` ) is inferred from`llm.prompt` being present; batch pipelines and user-facing agents look the same. Use suppression to opt out.\n\nTraccia merges configuration from multiple sources with the following priority (highest to lowest):\n\n1. **Explicit parameters** —`init(endpoint=\"...\", agent_id=\"...\")` or`start_tracing(...)`\n2. **Environment variables** —`TRACCIA_ENDPOINT` ,`TRACCIA_AGENT_ID` , etc.\n3. **Config file** —`traccia.toml` (current directory) or`~/.traccia/config.toml`\n4. **Defaults** — Built-in SDK defaults\n\n**Example**: If you set `TRACCIA_ENDPOINT` in your environment *and* pass `endpoint=...` to `init()`, the explicit parameter wins.\n\nCreate a `traccia.toml` file in your project root:\n\n```\ntraccia config init\n```\n\nThis creates a template config file:\n\n```\n[tracing]\n# API key — required for the Traccia platform, not needed for local OTLP backends\napi_key = \"\"\n\n# Endpoint URL for OTLP trace ingestion (default: Traccia platform)\n# For local OTLP backends use e.g. endpoint = \"http://localhost:4318/v1/traces\"\nendpoint = \"https://api.traccia.ai/v2/traces\"\n\nsample_rate = 1.0           # 0.0 to 1.0\nauto_start_trace = true     # Auto-start root trace on init\nauto_trace_name = \"root\"    # Name for auto-started trace\nuse_otlp = true             # Use OTLP exporter\n# service_name = \"my-app\"   # Optional service name\n# service_role has no env var — pass via init(service_role=\"orchestrator\")\n\n[exporters]\n# Only enable ONE exporter at a time\nenable_console = false        # Print traces to console\nenable_file = false           # Write traces to file\nfile_exporter_path = \"traces.jsonl\"\nreset_trace_file = false      # Reset file on initialization\n\n[instrumentation]\nenable_patching = true          # Auto-patch libraries (OpenAI, Anthropic, requests)\nenable_token_counting = true    # Count tokens for LLM calls\nenable_costs = true             # Calculate costs\nopenai_agents = true            # Auto-enable OpenAI Agents SDK integration\ncrewai = true                   # Auto-enable CrewAI integration\nguardrail_heuristics = true     # Tier C heuristic guardrail detection (tool error keywords)\nauto_instrument_tools = false   # Auto-instrument tool calls (experimental)\nmax_tool_spans = 100            # Max tool spans to create\nmax_span_depth = 10             # Max nested span depth\n\n[rate_limiting]\n# Optional: limit spans per second\n# max_spans_per_second = 100.0\nmax_queue_size = 5000           # Max buffered spans\nmax_block_ms = 100              # Max ms to block before dropping\nmax_export_batch_size = 512     # Spans per export batch\nschedule_delay_millis = 5000    # Delay between batches\n\n[metrics]\nenable_metrics = true           # Enable OpenTelemetry metrics\n# metrics_endpoint = \"\"         # Defaults to {traces_base}/v2/metrics\nmetrics_sample_rate = 1.0       # Metrics sampling rate (1.0 = 100%)\n\n[runtime]\n# Optional runtime metadata (agent identity: prefer init(agent_id=..., agent_name=..., env=...) or TRACCIA_* env)\n# session_id = \"\"\n# user_id = \"\"\n# tenant_id = \"\"\n# project_id = \"\"\n# agent_id = \"\"   # Single-agent: set in code or TRACCIA_AGENT_ID\n# agent_name = \"\"\n# env = \"\"        # e.g. production, staging, dev\n\n[logging]\ndebug = false                   # Enable debug logging\nenable_span_logging = false     # Enable span-level logging\n\n[advanced]\n# attr_truncation_limit = 1000  # Max attribute value length\n```\n\nIf you do not set `endpoint` (in config, environment, or when calling `init()` / `start_tracing()`), the SDK uses the **Traccia platform** by default (` https://api.traccia.ai/v2/traces`). You can override it to send traces to your own OTLP-compatible backend.\n\nThe default is defined in `traccia.config`: `DEFAULT_OTLP_TRACE_ENDPOINT`. The alias `DEFAULT_ENDPOINT` is kept for backward compatibility (same value).\n\nTraccia is fully OTLP-compatible and works with:\n\n- **Grafana Tempo** -`http://tempo:4318/v1/traces`\n- **Jaeger** -`http://jaeger:4318/v1/traces`\n- **Zipkin** - Configure via OTLP endpoint\n- **SigNoz** - Self-hosted observability platform\n- **Traccia Platform** -`https://api.traccia.ai/v2/traces` (requires API key)\n\nAll config parameters can be set via environment variables with the `TRACCIA_` prefix:\n\n**Tracing**: `TRACCIA_API_KEY`, `TRACCIA_ENDPOINT`, `TRACCIA_SAMPLE_RATE`, `TRACCIA_AUTO_START_TRACE`, `TRACCIA_AUTO_TRACE_NAME`, `TRACCIA_USE_OTLP`, `TRACCIA_SERVICE_NAME`\n\n**Exporters**: `TRACCIA_ENABLE_CONSOLE`, `TRACCIA_ENABLE_FILE`, `TRACCIA_FILE_PATH`, `TRACCIA_RESET_TRACE_FILE`\n\n**Instrumentation**: `TRACCIA_ENABLE_PATCHING`, `TRACCIA_ENABLE_TOKEN_COUNTING`, `TRACCIA_ENABLE_COSTS`, `TRACCIA_AUTO_INSTRUMENT_TOOLS`, `TRACCIA_MAX_TOOL_SPANS`, `TRACCIA_MAX_SPAN_DEPTH`, `TRACCIA_OPENAI_AGENTS`, `TRACCIA_CREWAI`, `TRACCIA_GUARDRAIL_HEURISTICS`\n\n**Rate Limiting**: `TRACCIA_MAX_SPANS_PER_SECOND`, `TRACCIA_MAX_QUEUE_SIZE`, `TRACCIA_MAX_BLOCK_MS`, `TRACCIA_MAX_EXPORT_BATCH_SIZE`, `TRACCIA_SCHEDULE_DELAY_MILLIS`\n\n**Runtime**: `TRACCIA_SESSION_ID`, `TRACCIA_USER_ID`, `TRACCIA_TENANT_ID`, `TRACCIA_PROJECT_ID`, `TRACCIA_AGENT_ID`, `TRACCIA_AGENT_NAME`, `TRACCIA_ENV`\n\nLegacy alias: `TRACCIA_PROJECT` (maps to `project_id`)\n\n**Logging**: `TRACCIA_DEBUG`, `TRACCIA_ENABLE_SPAN_LOGGING`\n\n**Advanced**: `TRACCIA_ATTR_TRUNCATION_LIMIT`\n\n``` python\nfrom traccia import init\n\n# Override config programmatically (including agent identity for single-agent services)\ninit(\n    endpoint=\"http://tempo:4318/v1/traces\",\n    sample_rate=0.5,\n    enable_costs=True,\n    max_spans_per_second=100.0,\n    agent_id=\"my-agent\",\n    agent_name=\"My Agent\",\n    env=\"production\",\n)\n```\n\nFor services that orchestrate many logical agents in one process, set a service role and scope per-run identity:\n\n``` python\nfrom traccia import init, runtime_config\n\ninit(\n    service_name=\"my-multi-agent-api\",\n    service_role=\"orchestrator\",\n    auto_start_trace=False,\n)\n\nwith runtime_config.run_identity(agent_id=\"billing-agent\", agent_name=\"Billing Agent\", env=\"production\"):\n    # run one logical agent task\n    ...\n```\n\nThis prevents the host service from being registered as a synthetic agent in the Traccia platform.\n\nIf one **process** runs many agents concurrently (for example, an API server or orchestrator), use this pattern:\n\n- Call `init()`**once per process** (for example at startup), not per request.\n- Wrap each logical \"run\" in `runtime_config.run_identity(...)` to set agent id/name/env for that run.\n- Do **not** call`stop_tracing()` per request; use`force_flush()` to flush spans/metrics after a run without shutting down the provider.\n\n``` python\nfrom traccia import init, span, force_flush, runtime_config\n\ninit(service_name=\"multi-agent-service\", auto_start_trace=False)\n\ndef run_agent(agent_id: str, env: str, payload: dict):\n    # Scope identity to this run only\n    with runtime_config.run_identity(agent_id=agent_id, agent_name=agent_id, env=env):\n        with span(\"agent.run\") as root:\n            root.set_attribute(\"agent.id\", agent_id)\n            root.set_attribute(\"agent.run.mode\", \"api\")\n            # ... your agent logic here ...\n\n    # Flush without tearing down the global provider\n    force_flush(5.0)\n```\n\nThe `@observe` decorator is the primary way to instrument your code:\n\n``` python\nfrom traccia import observe\n\n# Basic usage\n@observe()\ndef process_data(data):\n    return transform(data)\n\n# Custom span name\n@observe(name=\"data_pipeline\")\ndef process_data(data):\n    return transform(data)\n\n# Add custom attributes\n@observe(attributes={\"version\": \"2.0\", \"env\": \"prod\"})\ndef process_data(data):\n    return transform(data)\n\n# Specify span type\n@observe(as_type=\"llm\")  # \"span\", \"llm\", \"tool\"\ndef call_llm():\n    pass\n\n# Skip capturing specific arguments\n@observe(skip_args=[\"password\", \"secret\"])\ndef authenticate(username, password):\n    pass\n\n# Skip capturing result (for large returns)\n@observe(skip_result=True)\ndef fetch_large_dataset():\n    return huge_data\n```\n\n**Available Parameters**:\n\n- `name` (str, optional): Custom span name (defaults to function name)\n- `attributes` (dict, optional): Initial span attributes\n- `as_type` (str): Span type -`\"span\"` ,`\"llm\"` ,`\"tool\"` , or`\"guardrail\"`\n- `skip_args` (list, optional): List of argument names to skip capturing\n- `skip_result` (bool): Skip capturing the return value\n\n`@observe` works seamlessly with async functions:\n\n``` python\n@observe()\nasync def async_task(x):\n    await asyncio.sleep(1)\n    return x * 2\n\nresult = await async_task(5)\n```\n\nFor more control, create spans manually:\n\n``` python\nfrom traccia import get_tracer, span\n\n# Using convenience function\nwith span(\"operation_name\") as s:\n    s.set_attribute(\"key\", \"value\")\n    s.add_event(\"checkpoint_reached\")\n    do_work()\n\n# Using tracer directly\ntracer = get_tracer(\"my_service\")\nwith tracer.start_as_current_span(\"operation\") as s:\n    s.set_attribute(\"user_id\", 123)\n    do_work()\n```\n\nTraccia automatically captures and records errors:\n\n``` python\n@observe()\ndef failing_function():\n    raise ValueError(\"Something went wrong\")\n\n# Span will contain:\n# - error.type: \"ValueError\"\n# - error.message: \"Something went wrong\"\n# - error.stack_trace: (truncated stack trace)\n# - span status: ERROR\n```\n\nSpans are automatically nested based on call hierarchy:\n\n``` python\n@observe()\ndef parent_operation():\n    child_operation()\n    return \"done\"\n\n@observe()\ndef child_operation():\n    grandchild_operation()\n\n@observe()\ndef grandchild_operation():\n    pass\n\n# Creates nested span hierarchy:\n# parent_operation\n#   └── child_operation\n#       └── grandchild_operation\n```\n\nTraccia includes a powerful CLI for configuration and diagnostics:\n\nCreate a new `traccia.toml` configuration file:\n\n```\ntraccia config init\ntraccia config init --force  # Overwrite existing\n```\n\nValidate configuration and diagnose issues:\n\n```\ntraccia doctor\n\n# Output:\n# 🩺 Running Traccia configuration diagnostics...\n# \n# ✅ Found config file: ./traccia.toml\n# ✅ Configuration is valid\n# \n# 📊 Configuration summary:\n#    • API Key: ❌ Not set (optional)\n#    • Endpoint: https://api.traccia.ai/v2/traces\n#    • Sample Rate: 1.0\n#    • OTLP Exporter: ✅ Enabled\n```\n\nTest connectivity to your exporter endpoint:\n\n```\ntraccia check\ntraccia check --endpoint http://tempo:4318/v1/traces\n```\n\nManage the local LLM pricing snapshot used for cost estimation:\n\n```\n# Show current pricing source, age, and model count\ntraccia pricing status\n\n# Download the latest pricing — tries the Traccia platform first,\n# automatically falls back to the upstream pricing source if the platform\n# is unreachable (e.g. no Traccia account, network error)\ntraccia pricing refresh\n\n# Force-fetch directly from the upstream pricing source (skips the platform)\ntraccia pricing refresh --source upstream\n\n# Remove local cache — reverts to the bundled snapshot shipped with the SDK\ntraccia pricing clear\n```\n\nTraccia computes estimated LLM costs locally at span-end time using a pricing table that resolves in the following order (highest precedence first):\n\n| Priority | Source | Set via | \n|---|---|---|\n| 1 | **Programmatic override** | `start_tracing(pricing_override={...})` | \n| 2 | **Env var override** | `TRACCIA_PRICING_OVERRIDE_JSON='{\"model\": {\"prompt\": x, \"completion\": y}}'` | \n| 3 | **Local cache** | `traccia pricing refresh` (stored in`~/.cache/traccia/pricing.json` ) | \n| 4 | **Bundled snapshot** | Ships inside the SDK wheel (generated from LiteLLM at release time) | \n\nThe bundled snapshot is generated at SDK release time from an upstream pricing\ndatabase (2500+ models). It is refreshed automatically on every SDK release via\nthe CI/CD pipeline — so `pip install --upgrade traccia` always brings a\nreasonably current snapshot.\n\nBetween releases, you can pull the latest pricing without upgrading the SDK:\n\n```\ntraccia pricing refresh    # platform → upstream fallback\n```\n\nThe SDK warns you when the active snapshot is outdated:\n\n- **>7 days old:**`logger.info` — reminder to refresh\n- **>30 days old:**`logger.warning` — includes the exact command\n\nEvery span also carries these attributes so the platform can always see how fresh the SDK's pricing was at emit time:\n\n| Attribute | Description | \n|---|---|\n| `llm.pricing.generated_at` | ISO timestamp of the pricing snapshot | \n| `llm.pricing.age_days` | Integer age in days at emit time | \n| `llm.pricing.source` | One of `bundled \\| local_cache \\| env \\| override` | \n\nIf you use the Traccia platform, it maintains its own pricing sync (every 6 hours by default). Costs shown in the platform UI are recomputed using the platform's authoritative snapshot, which is always fresher than the SDK's bundled table:\n\n```\norg override > platform snapshot > bundled fallback\n```\n\nWhen the platform recomputes a cost, it writes `platform_cost_usd` alongside the\noriginal `llm.cost.usd` on each span — the SDK-reported value is **never\noverwritten**. In the span detail panel you will see both values clearly labeled:\n\n- **\"Traccia recomputed cost\"** — what the platform computed, with a tooltip\nshowing whether it came from an automatic pricing update or an org override.\n- **\"SDK-side pricing context (at emit time)\"** — the`llm.pricing.*` attributes,\ncollapsed by default, showing what prices the SDK saw when the span was emitted.\n\nUse `traccia pricing refresh` to sync the platform's snapshot into your SDK's local\ncache — then `llm.cost.usd` and `platform_cost_usd` will converge.\n\nYou may occasionally see a muted \"SDK reported $X.XX\" line on the costs page. This happens when the two sources used different rates, for three legitimate reasons:\n\n| Reason | How to resolve | \n|---|---|\n| SDK's local cache or bundled snapshot is slightly older than the platform's current snapshot | Temporary — platform catches up within 6 hours, or click \"Sync now\" in Settings → Pricing. Running `traccia pricing refresh` also syncs the SDK. | \n| SDK-level override set via `pricing_override=...` or`TRACCIA_PRICING_OVERRIDE_JSON` | Intentional — the SDK override applies only to `llm.cost.usd` for that process. The platform uses its own snapshot + org overrides for`platform_cost_usd` . | \n| Org override set in Settings → Pricing on the platform | Intentional — org overrides apply to `platform_cost_usd` for all agents in the org. They do not change what SDK processes report. | \n\nThe platform **never uses the SDK's local cache** as input to its own recomputation.\nEach agent process may have a different cached version, and the platform needs one\nconsistent source for cross-agent reporting. SDK pricing affects what gets emitted\nin spans; platform pricing determines what gets shown in aggregate dashboards.\n\nAll pricing overrides — whether set programmatically, via environment variable, or via the Traccia platform settings — use the same JSON shape:\n\n```\n{\n  \"<model-name>\": {\n    \"prompt\": 0.005,\n    \"completion\": 0.015,\n    \"cache_write\": 0.001,\n    \"cached_prompt\": 0.0005\n  }\n}\n```\n\nAll rates are **USD per 1,000 tokens**. `cache_write` and `cached_prompt` are\noptional (for models with prompt-caching pricing, e.g. Claude 3.5).\n\nOverrides always win over any other source:\n\n```\n# Programmatic (per-process) — both span attributes and cost metrics will use this\ntraccia.start_tracing(\n    pricing_override={\n        \"gpt-4o\": {\"prompt\": 0.005, \"completion\": 0.015},\n        \"my-fine-tuned-model\": {\"prompt\": 0.01, \"completion\": 0.02},\n    }\n)\n# Environment variable (persistent across restarts)\nexport TRACCIA_PRICING_OVERRIDE_JSON='{\"gpt-4o\": {\"prompt\": 0.005, \"completion\": 0.015}}'\n```\n\n**Deprecation notice:** `AGENT_DASHBOARD_PRICING_JSON` is accepted as a\nback-compat alias for `TRACCIA_PRICING_OVERRIDE_JSON` but will be removed in\na future minor version. Rename the variable in your environment.\n\n**Platform overrides (org-level):** Org admins can set pricing overrides in\n**Settings → Pricing** on the Traccia platform. These apply to the\nplatform-recomputed cost (`platform_cost_usd`) for all agents in the org. They\ndo not change `llm.cost.usd` on existing spans retroactively unless you\nexplicitly enable the \"Also recompute past traces\" option in the save dialog.\n\nProtect your infrastructure with built-in rate limiting:\n\n```\n[rate_limiting]\nmax_spans_per_second = 100.0  # Limit to 100 spans/sec\nmax_queue_size = 5000         # Max buffered spans\nmax_block_ms = 100             # Block up to 100ms before dropping\n```\n\n**Behavior**:\n\n1. Try to acquire capacity immediately\n2. If unavailable, block for up to `max_block_ms`\n3. If still unavailable, drop span and log warning\n\nWhen spans are dropped due to rate limiting, warnings are logged to help you monitor and adjust limits.\n\nControl trace volume with sampling:\n\n```\n# Sample 10% of traces\ninit(sample_rate=0.1)\n\n# Sampling is applied at trace creation time\n# Traces are either fully included or fully excluded\n```\n\nAutomatic for supported LLM providers (OpenAI, Anthropic):\n\n``` python\n@observe(as_type=\"llm\")\ndef call_openai(prompt):\n    response = client.chat.completions.create(\n        model=\"gpt-4\",\n        messages=[{\"role\": \"user\", \"content\": prompt}]\n    )\n    return response.choices[0].message.content\n\n# Span automatically includes:\n# - llm.token.prompt_tokens\n# - llm.token.completion_tokens\n# - llm.token.total_tokens\n# - llm.cost.total (in USD)\n```\n\nTraccia emits OTEL-compliant metrics for accurate cost and token tracking, independent of trace sampling.\n\nWith trace sampling (e.g., `sample_rate=0.1`), only 10% of traces are exported. Cost calculated from traces will be **10x underestimated**. Metrics solve this by recording data for **every** LLM call, regardless of sampling.\n\nTraccia automatically emits these metrics:\n\n| Metric | Type | Unit | Description | \n|---|---|---|---|\n| `gen_ai.client.token.usage` | Histogram | `{token}` | Input/output tokens per call | \n| `gen_ai.client.operation.duration` | Histogram | `s` | LLM operation duration | \n| `gen_ai.client.operation.cost` | Histogram | `usd` | Cost per call (USD) | \n| `gen_ai.client.completions.exceptions` | Counter | `1` | Exception count | \n| `gen_ai.agent.runs` | Counter | `1` | Agent runs (CrewAI, OpenAI Agents) | \n| `gen_ai.agent.turns` | Counter | `1` | Agent turns | \n| `gen_ai.agent.execution_time` | Histogram | `s` | Agent execution time | \n\n**Attributes**: `gen_ai.system` (openai, anthropic), `gen_ai.request.model`, `gen_ai.agent.id`, `gen_ai.agent.name`\n\n``` python\nfrom traccia import init\n\ninit(\n    enable_metrics=True,  # Default: True\n    metrics_endpoint=\"https://your-backend.com/v2/metrics\",  \n    metrics_sample_rate=1.0,  # Default: 1.0 (100%)\n)\n```\n\nOr via `traccia.toml`:\n\n```\n[metrics]\nenable_metrics = true\nmetrics_endpoint = \"https://your-backend.com/v2/metrics\"\nmetrics_sample_rate = 1.0\n```\n\nOr via environment variables:\n\n```\nexport TRACCIA_ENABLE_METRICS=true\nexport TRACCIA_METRICS_ENDPOINT=https://your-backend.com/v2/metrics\nexport TRACCIA_METRICS_SAMPLE_RATE=1.0\n```\n\nRecord your own metrics:\n\n``` python\nfrom traccia.metrics import record_counter, record_histogram\n\n# Record a counter\nrecord_counter(\"my_custom_events\", 1, {\"event_type\": \"user_action\"})\n\n# Record a histogram\nrecord_histogram(\"my_custom_latency\", 0.123, {\"service\": \"api\"}, unit=\"s\")\n```\n\nAgent-level metrics (such as `gen_ai.agent.runs` and `gen_ai.agent.execution_time`) are only emitted when Traccia can\nsee a real **agent lifecycle** (for example, CrewAI crews or OpenAI Agents SDK runs). For plain OpenAI/Anthropic calls\nand most simple LangChain usages, you will still get full LLM metrics (`gen_ai.client.*`), but no agent metrics unless\nyou build an explicit agent abstraction on top.\n\n``` python\nimport logging\nlogging.basicConfig(level=logging.DEBUG)\n\n# Or via config\ninit(debug=True)\n\n# Or via env var\n# TRACCIA_DEBUG=1 python your_script.py\n```\n\n1. Check connectivity: `traccia check`\n2. Validate config: `traccia doctor`\n3. Enable debug logging\n4. Verify endpoint is correct and accessible\n\n- Reduce `max_queue_size` in rate limiting config\n- Lower `sample_rate` to reduce volume\n- Enable rate limiting with `max_spans_per_second`\n\n- Check rate limiter logs for warnings\n- Increase `max_spans_per_second` if set\n- Increase `max_queue_size` if spans are queued\n- Check `traccia doctor` output\n\nInitialize the Traccia SDK. All parameters are optional; configuration is merged from `traccia.toml` → env vars → explicit parameters (highest wins).\n\n**Parameters**:\n\n*Tracing*\n\n- `endpoint` (str): OTLP endpoint URL (default:`https://api.traccia.ai/v2/traces` )\n- `api_key` (str): API key for the Traccia platform\n- `sample_rate` (float): Sampling rate 0.0–1.0 (default: 1.0)\n- `auto_start_trace` (bool): Auto-start a root trace on init (default: True)\n- `auto_trace_name` (str): Name for the auto-started trace (default:`\"root\"` )\n- `use_otlp` (bool): Use OTLP exporter (default: True)\n- `service_name` (str): Service name (auto-detected if not set)\n- `service_role` (str):`\"orchestrator\"` to prevent this service being registered as an agent\n- `config_file` (str): Path to a custom`traccia.toml`\n\n*Exporters*\n\n- `enable_console_exporter` (bool): Print spans to stdout (default: False)\n- `enable_file_exporter` (bool): Write spans to file (default: False)\n- `file_exporter_path` (str): Path for file exporter (default:`\"traces.jsonl\"` )\n- `reset_trace_file` (bool): Clear file on init (default: False)\n\n*Instrumentation*\n\n- `enable_patching` (bool): Auto-patch OpenAI, Anthropic, requests (default: True)\n- `enable_token_counting` (bool): Count tokens (default: True)\n- `enable_costs` (bool): Calculate costs (default: True)\n- `pricing_override` (dict): Per-model pricing override — always wins over all other sources. See[Pricing](#pricing) section below.\n- `openai_agents` (bool): Auto-enable OpenAI Agents SDK integration (default: True)\n- `crewai` (bool): Auto-enable CrewAI integration (default: True)\n- `guardrail_heuristics` (bool): Enable Tier C heuristic guardrail detection (default: True)\n- `auto_instrument_tools` (bool): Experimental tool auto-instrumentation (default: False)\n- `max_tool_spans` (int): Max tool spans per trace (default: 100)\n- `max_span_depth` (int): Max nested span depth (default: 10)\n\n*Agent identity (single-agent services)*\n\n- `agent_id` (str): Logical agent identifier\n- `agent_name` (str): Human-readable agent name\n- `env` (str): Deployment environment, e.g.`\"production\"` ,`\"staging\"`\n\n*Runtime metadata*\n\n- `session_id` (str): Session identifier\n- `user_id` (str): User identifier\n- `tenant_id` (str): Tenant / org identifier\n- `project_id` (str): Project identifier\n\n*Metrics*\n\n- `enable_metrics` (bool): Emit OTEL metrics (default: True)\n- `metrics_endpoint` (str): Metrics endpoint (derived from tracing endpoint if not set)\n- `metrics_sample_rate` (float): Metrics sampling rate (default: 1.0)\n\n*Rate limiting*\n\n- `max_spans_per_second` (float): Rate limit spans/sec (default: None = unlimited)\n- `max_block_ms` (int): Max ms to block before dropping a span (default: 100)\n- `max_queue_size` (int): Max buffered spans (default: 5000)\n- `max_export_batch_size` (int): Spans per export batch (default: 512)\n- `schedule_delay_millis` (int): Batch export interval ms (default: 5000)\n\n*Misc*\n\n- `debug` (bool): Enable debug logging (default: False)\n- `attr_truncation_limit` (int): Max attribute value length (default: None)\n\n**Returns**: TracerProvider instance\n\nFetch a versioned prompt from Traccia (cache + SWR + optional fallback). See [Prompts in the SDK](https://traccia.ai/docs/sdk/prompts).\n\n``` python\nfrom traccia import load_prompt, prefetch_prompts\n\nprefetch_prompts([\"support-reply\"])\nprompt = load_prompt(\"support-reply\", label=\"production\", fallback={...})\nmessages = prompt.compile(question=q)\n```\n\n#### `evaluate(name, *, data, task, scorers=None, prompt=None, max_concurrency=10, persist=True, provider_keys=None, progress=True, ...) -> EvaluateResult`\n\nRun a task and scorers over a platform dataset or inline rows. Persists an experiment by default. See [Evaluate in the SDK](https://traccia.ai/docs/sdk/evaluate).\n\n``` python\nfrom traccia import evaluate\n\nresult = evaluate(\n    \"support-reply-v3\",\n    data=\"support-golden\",\n    task=task,\n    scorers=[\"exact_match\", \"contains\"],\n    prompt=\"support-reply\",\n    persist=True,\n)\nprint(result.summary())\nprint(result.url)\n```\n\n**Parameters**:\n\n- `name` (str): Experiment name (required)\n- `data` : Dataset name/UUID, or a list of`{input, expected, metadata}` dicts (`expected_output` is an alias of`expected` )\n- `task` : Callable mapping each row to an output. First arg named`row` /`item` /`example` /`case` receives the full row; otherwise the input dict\n- `scorers` : Builtin ids (`exact_match` ,`contains` ,`json_valid` ), platform names/ids, and/or callables`(input=, output=, expected=, metadata=)`\n- `prompt` (str, optional): Prompt name. Attaches version ids when persisting and labels the cell (otherwise`Task` )\n- `max_concurrency` (int): Parallel item workers (default 10)\n- `persist` (bool): Create an experiment (default True). Inline + persist creates`sdk-eval/<name>/<id>`\n- `provider_keys` (dict, optional): BYO keys for platform LLM-as-judge (`openai` ,`anthropic` ,`gemini` ,`groq` ) and`typesafe` for Jev Decision. A Jev score keeps each question, so the experiment shows one chip per question.\n- `progress` (bool): Print`N/M` to stderr (default True)\n\n**Returns**: `EvaluateResult` with `rows`, `aggregates`, `summary()`, `url`, `experiment_id`, `dataset_id`, `errors`, `persist_error`. Empty data, bad config, and dataset/scorer API failures raise `EvaluateError`.\n\nStop tracing and flush pending spans.\n\n**Parameters**:\n\n- `flush_timeout` (float): Max seconds to wait for flush\n\nGet a tracer instance.\n\n**Parameters**:\n\n- `name` (str): Tracer name (typically module/service name)\n\n**Returns**: Tracer instance\n\nCreate a span context manager.\n\n**Parameters**:\n\n- `name` (str): Span name\n- `attributes` (dict, optional): Initial attributes\n\n**Returns**: Span context manager\n\n#### `@observe(name=None, *, attributes=None, tags=None, as_type=\"span\", skip_args=None, skip_result=False)`\n\nDecorate a function to create spans automatically.\n\n**Parameters**:\n\n- `name` (str, optional): Span name (default: function name)\n- `attributes` (dict, optional): Initial attributes\n- `tags` (list[str], optional): User-defined identifiers for the observed method\n- `as_type` (str): Span type (`\"span\"` ,`\"llm\"` ,`\"tool\"` ,`\"guardrail\"` )\n- `skip_args` (list, optional): Arguments to skip capturing\n- `skip_result` (bool): Skip capturing return value\n\nLoad and validate configuration.\n\n**Parameters**:\n\n- `config_file` (str, optional): Path to config file\n- `overrides` (dict, optional): Override values\n\n**Returns**: Validated TracciaConfig instance\n\n**Raises**: `ConfigError` if invalid\n\nValidate configuration without loading.\n\n**Returns**: Tuple of (is_valid, message, config_or_none)\n\n```\nApplication Code (@observe)\n        ↓\n   Span Creation\n        ↓\n   Processors (token counting, cost, enrichment)\n        ↓\n   Rate Limiter (optional)\n        ↓\n   Batch Processor (buffering)\n        ↓\n   Exporter (OTLP/Console/File)\n        ↓\n   Backend (Grafana Tempo / Jaeger / Zipkin / etc.)\n```\n\n- \n**`traccia.instrumentation.*`** : Infrastructure and vendor instrumentation.\n  - HTTP client/server helpers (including FastAPI middleware).\n  - Vendor SDK hooks and monkey patching (e.g., OpenAI, Anthropic, Gemini, `requests` ).\n  - Decorators and utilities used for auto-instrumenting arbitrary functions.\n- \n**`traccia.integrations.*`** : AI/agent framework integrations.\n  - Adapters that plug into higher-level frameworks via their official extension points (e.g., LangChain callbacks).\n  - Work at the level of chains, tools, agents, and workflows rather than raw HTTP or SDK calls.\n\nContributions are welcome! Whether it's bug fixes, new features, documentation improvements, or examples - we appreciate your help.\n\n1. **Fork the repository**\n2. **Create a feature branch** :`git checkout -b feature/amazing-feature`\n3. **Make your changes** and add tests\n4. **Run tests** :`pytest tests/`\n5. **Lint your code** :`ruff check traccia/`\n6. **Commit** :`git commit -m \"Add amazing feature\"`\n7. **Push** :`git push origin feature/amazing-feature`\n8. **Open a Pull Request**\n\n```\n# Clone the repository (Python SDK)\ngit clone https://github.com/traccia-ai/traccia-py.git\ncd traccia-py\n\n# Create virtual environment\npython -m venv venv\nsource venv/bin/activate  # On Windows: venv\\Scripts\\activate\n\n# Install in editable mode with dev dependencies\npip install -e \".[dev]\"\n\n# Run tests\npytest tests/ -v\n\n# Run with coverage\npytest tests/ --cov=traccia --cov-report=html\n```\n\n- Follow PEP 8\n- Use type hints where appropriate\n- Add docstrings for public APIs\n- Write tests for new features\n- Keep PRs focused and atomic\n\n- **Integrations** : Add support for more LLM providers (Cohere, AI21, local models)\n- **Backends** : Test and document setup with different OTLP backends\n- **Examples** : Real-world examples of agent instrumentation\n- **Documentation** : Tutorials, guides, video walkthroughs\n- **Performance** : Optimize hot paths, reduce overhead\n- **Testing** : Improve test coverage, add integration tests\n\nTraccia adds **GovernanceEvent** attributes on spans automatically (event type, integrity hash, timestamp source). No EU-specific setup is required for default tracing. **Human review** (approve/reject, audit trail) is done in the **Governance Hub** on the platform — not in the SDK.\n\n| Decorator | Purpose | Requires Traccia platform | \n|---|---|---|\n| `@observe` | Observability only — creates trace spans | No (works with any OTLP backend) | \n| `@govern` | Observability **plus** runtime policy enforcement | **Yes** | \n\n`@govern` does two things:\n\n1. Checks agent status before the function body (can warn or stop the **next** run for after-ingest policies).\n2. Turns on a per-call check for **this** LLM or tool call (Spend Cap, Model Boundary, Loop Cap). You do not call`check_policy()` yourself when using an instrumented LLM client and`@observe(as_type=\"tool\")` .\n\nTracing-only or self-hosted users should use `@observe`.\n\n``` python\nfrom openai import OpenAI\nfrom traccia import init, observe, govern, AgentBlockedError\n\ninit(api_key=\"...\", endpoint=\"https://api.traccia.ai/v2/traces\", agent_id=\"my-agent\")\nclient = OpenAI()\n\n@observe(name=\"lookup_order\", as_type=\"tool\")\ndef lookup_order(order_id: str) -> dict:\n    return {\"order_id\": order_id, \"status\": \"shipped\"}\n\n@govern(name=\"run_agent\")\ndef run_agent(prompt: str) -> str:\n    lookup_order(\"ORD-1\")\n    resp = client.chat.completions.create(\n        model=\"gpt-4o\",\n        messages=[{\"role\": \"user\", \"content\": prompt}],\n    )\n    return resp.choices[0].message.content or \"\"\n\ntry:\n    run_agent(\"hello\")\nexcept AgentBlockedError as exc:\n    print(exc.reasons, exc.decision_id, exc.remaining_budget_usd)\n```\n\nIdentity comes from `init(agent_id=...)` (or `TRACCIA_AGENT_ID`). Pass `agent_id` on `@govern` only to override in a multi-agent process. Without an agent ID, API key or endpoint, `@govern` raises `GovernanceConfigError` (a subclass of `AgentBlockedError`) instead of running unchecked. On Block deny, `@govern` raises `AgentBlockedError`. Observe and Warn still let the call proceed and record a match. Refund Guard and Purchase Guard can hold a middle amount: `@govern` raises `ApprovalPending`, which is not `AgentBlockedError`. Catch it, do not run the tool, and do not retry the check. `pending_tool_result()` is a normal tool result when a framework retries raised errors.\n\nFor a custom tool that is **not** wrapped with `@observe(as_type=\"tool\")`, call `check_policy(action={\"type\": \"tool_call\", \"name\": \"refund\"}, context={\"input\": {\"amount\": amount}})` yourself.\n\nIf a check gets no answer (timeout, connection error, 5xx, or an invalid answer), the call follows your **policy fail mode**: **Settings → Policy Enforcement** in the app, for the organization or one workspace. The default is to block: the call raises `AgentBlockedError`, and the provider or tool is never called. Choose Allow there to let calls through while Traccia is down.\n\n- The setting arrives with answers the SDK already gets: the agent status at the start of each `@govern` run (cached 60 s) and each policy check. There are no extra requests, and tracing-only code never calls Traccia for it.\n- It is kept for 10 minutes after the last answer that carried it, then the SDK goes back to blocking. A process that has had no answer yet, for example one that starts while Traccia is down, blocks. Calling `init()` again forgets it.\n- A local override applies only when there is no answer: `init(policy_fail_mode=\"open\" | \"closed\")` for the process, or`@govern(policy_fail_mode=...)` for one run and the checks inside it. It never overrides a rejected API key (401/403), a block, a deny or a configuration error. The SDK logs a warning once when it disagrees with your Traccia setting, since the[Traccia Gateway](https://traccia.ai/docs/platform/gateway) follows the setting.\n- `@govern(fail_open=...)` is deprecated:`True` means`policy_fail_mode=\"open\"` ,`False` means`\"closed\"` . Omit it to follow your setting.\n\nPolicies in the app: [Policies](https://traccia.ai/docs/platform/policies). SDK guide: [Governance in the SDK](https://traccia.ai/docs/sdk/governance).\n\n**Advanced (optional):** override endpoints or cache TTL in `traccia.toml`:\n\n```\n[governance]\nstatus_check_endpoint = \"https://custom.example/agents/{agent_id}/status\"\npost_block_endpoint = \"https://custom.example/agents/{agent_id}/blocks\"\nstatus_cache_ttl_seconds = 120\npolicy_fail_mode = \"closed\"   # optional local override, see above\n```\n\nOr pass `status_check_endpoint`, `post_block_endpoint`, `status_cache_ttl_seconds`, or `policy_fail_mode` to `init()`.\n\nQueue and decide reviews in **Governance Hub → Reviews** using a **trace ID**. The SDK does not create review queue rows. Use the dashboard when a person must sign off before you rely on an automated output.\n\nWhen your product **shows** the user they are interacting with AI (banner, label, chat notice) or synthetic content, call `disclosure()` on the active span so exported traces carry audit evidence (`governance.transparency.disclosed`, `governance.event_type=transparency`). It does **not** render UI.\n\n``` python\nfrom traccia.governance import disclosure\n\ndisclosure(channel=\"ui\", disclosed_to_user=True)  # after user saw your AI notice\n```\n\n`init(compliance={\"frameworks\": [\"eu_ai_act\"], \"risk_tier\": \"high\"})` stamps `eu_ai_act.risk_tier` on every span from that process. Static SDK config — not read from the dashboard registry. Link registry + agent in the UI for exports; set `risk_tier` in `init()` if you need it on every span.\n\nBest-effort regex masking (email, phone, SSN-like, MRN/NPI/DOB heuristics). **Not** a guarantee that all PHI is removed — not ML/medical NER.\n\n``` python\nfrom opentelemetry import trace\nfrom traccia import init, observe\nfrom traccia.processors.redaction_processor import redact_string, apply_redaction_to_span\n\n# Automatic: redact sensitive attribute keys on every span before export\ninit(redact_pii=True)  # or TRACCIA_REDACT_PII=1\n# When compliance includes hipaa, redact_pii defaults on unless explicitly set False\ninit(compliance={\"frameworks\": [\"hipaa\"]}, redact_pii=True)\n\n@observe()\ndef agent_step(user_message: str) -> str:\n    span = trace.get_current_span()\n    span.set_attribute(\"gen_ai.prompt\", redact_string(user_message))  # manual\n    out = run_llm(user_message)\n    apply_redaction_to_span(span)  # or rely on init(redact_pii=True)\n    return out\n```\n\n- **Platform** :[Governance Hub](https://traccia.ai/docs/platform/governance) — registry, reviews, incidents, evidence exports.\n- **EU AI Act guide** :[docs/compliance/eu-ai-act](https://traccia.ai/docs/compliance/eu-ai-act) (opt-in module; decision support only).\n- **HIPAA guide** :[docs/compliance/hipaa](https://traccia.ai/docs/compliance/hipaa) (opt-in module; soft PHI warnings; no signed BAA yet — contact[support@traccia.ai](mailto:support@traccia.ai) ).\n- **Trust Center** :[trust-center](https://traccia.ai/trust-center) (HIPAA shared responsibility).\n\nApache License 2.0 - see [LICENSE](https://github.com/traccia-ai/traccia-py/blob/main/LICENSE) for full terms and conditions.\n\nBuilt with:\n\n- [OpenTelemetry](https://opentelemetry.io/) - Vendor-neutral observability framework\n- [Pydantic](https://pydantic.dev/) - Data validation\n- [tiktoken](https://github.com/openai/tiktoken) - Token counting\n\nInspired by observability tools in the ecosystem and designed to work seamlessly with the OTLP standard.\n\n- **Issues** :[GitHub Issues](https://github.com/traccia-ai/traccia-py/issues) - Report bugs or request features\n- **Discussions** :[GitHub Discussions](https://github.com/traccia-ai/traccia-py/discussions) - Ask questions, share ideas\n\n**Made with ❤️ for the AI agent community**", "url": "https://wpnews.pro/news/show-hn-traccia-vendor-neutral-runtime-control-for-ai-agents", "canonical_source": "https://github.com/traccia-ai/traccia-py", "published_at": "2026-11-03 20:06:53+00:00", "updated_at": "2026-11-03 20:20:26.740346+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "mlops", "developer-tools"], "entities": ["Traccia", "OpenTelemetry", "OpenAI", "Anthropic", "Google Gemini", "LangChain", "CrewAI", "PyPI"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/show-hn-traccia-vendor-neutral-runtime-control-for-ai-agents", "markdown": "https://wpnews.pro/news/show-hn-traccia-vendor-neutral-runtime-control-for-ai-agents.md", "text": "https://wpnews.pro/news/show-hn-traccia-vendor-neutral-runtime-control-for-ai-agents.txt", "jsonld": "https://wpnews.pro/news/show-hn-traccia-vendor-neutral-runtime-control-for-ai-agents.jsonld"}}