Metadata-Version: 2.5
Name: virgo-observe
Version: 0.8.1
Summary: Low-friction Virgo instrumentation over standard OpenTelemetry
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: opentelemetry-api<2,>=1.39.1
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.39.1
Requires-Dist: opentelemetry-sdk<2,>=1.39.1
Provides-Extra: agno
Requires-Dist: openinference-instrumentation-agno==1.0.6; extra == 'agno'
Provides-Extra: anthropic
Requires-Dist: openinference-instrumentation-anthropic==2.1.1; extra == 'anthropic'
Provides-Extra: autogen
Requires-Dist: openinference-instrumentation-autogen-agentchat==0.1.14; extra == 'autogen'
Provides-Extra: claude-agent-sdk
Requires-Dist: claude-agent-sdk==0.2.104; extra == 'claude-agent-sdk'
Provides-Extra: codex
Requires-Dist: openai-codex==0.147.0; extra == 'codex'
Provides-Extra: crewai
Requires-Dist: openinference-instrumentation-crewai==1.1.15; extra == 'crewai'
Provides-Extra: dspy
Requires-Dist: openinference-instrumentation-dspy==0.1.42; extra == 'dspy'
Provides-Extra: fastmcp
Requires-Dist: fastmcp<5,>=3; extra == 'fastmcp'
Provides-Extra: langchain
Requires-Dist: openinference-instrumentation-langchain==0.1.73; extra == 'langchain'
Provides-Extra: langgraph
Requires-Dist: openinference-instrumentation-langchain==0.1.73; extra == 'langgraph'
Provides-Extra: litellm
Requires-Dist: openinference-instrumentation-litellm==0.1.40; extra == 'litellm'
Provides-Extra: llama-index
Requires-Dist: openinference-instrumentation-llama-index==4.4.8; extra == 'llama-index'
Provides-Extra: mcp-fastmcp
Requires-Dist: mcp<2,>=1.28.1; extra == 'mcp-fastmcp'
Provides-Extra: openai
Requires-Dist: openinference-instrumentation-openai==0.1.57; extra == 'openai'
Provides-Extra: openai-agents
Requires-Dist: openinference-instrumentation-openai-agents==2.2.0; extra == 'openai-agents'
Provides-Extra: pydantic-ai
Requires-Dist: pydantic-ai-slim<3,>=2.15; extra == 'pydantic-ai'
Provides-Extra: vercel-python
Requires-Dist: ai[otel]==0.5.2; extra == 'vercel-python'
Description-Content-Type: text/markdown

# virgo-observe

Four Python operations for Virgo: register a client, trace an operation, attach
feedback, and publish an individual product metric. Traces use OpenTelemetry;
feedback and metrics use unsampled HTTP channels authorized by the same Observe key.

Version **0.8.0** adds native Claude Agent SDK query, client-turn, assistant,
tool, streaming, and error observation. It keeps registration inactive by
default under pytest and retains the existing Codex, FastMCP, Pydantic AI, and
OpenInference integrations. Registration automatically discovers installed
supported integrations, reads saved bootstrap configuration, and drains
telemetry on normal Python exit.

Install the extras for the application's supported agent harnesses, for example:

```bash
uv add "virgo-observe[claude-agent-sdk]==0.8.0"
```

Then register once during startup:

```python
from virgo_observe import register

register(project_name="my-project")
```

No framework selection is required. Required adapters must be installed and
framework versions compatible. FastMCP needs its server-instance attachment;
Semantic Kernel needs its native telemetry setup, as documented below.
Standalone, non-agentic provider calls are optional for onboarding; add the
`openai` or `anthropic` extra if that additional coverage is wanted. Calls made
inside agent harnesses remain part of agent coverage.

Platform pins `virgo-observe[codex,openai,pydantic-ai]==0.7.2`, preserving
Pydantic AI 2.15.0 and openai-codex 0.147.0. SDK contributors can install the
checkout with `uv pip install ./packages/virgo-observe-python`.

## Four operations

Choose coverage in Virgo's “Trace with Virgo-Observe” setup and run its bootstrap
command. `register()` finds `.virgo/observe.env` from the working directory or
its parents, stopping at the current Git repository boundary. No application
`load_dotenv()` call is needed. Deployed runtimes can supply settings directly:

```bash
export VIRGO_API_KEY='<observe-key>'
```

Bootstrap saves the endpoint with the key. Saved settings are read only during
registration, do not mutate `os.environ`, and never evaluate shell expressions
or expand variable references. Only recognized Observe/OTel settings are read.
The file must be a regular, owner-only file on POSIX; symlinks and duplicate
settings are rejected. Explicit or process credentials select process/argument
configuration without reading the saved file. Other process settings override
saved values. The default endpoint is hosted Virgo; `endpoint=` and
`VIRGO_ENDPOINT` support custom deployments.

Without a key, `register()` returns a client with `enabled=False` and
`registration_report.reason_code == "missing_api_key"`. It installs no framework
instrumentation, starts no delivery workers and exports nothing. Manual spans
are non-recording; feedback/metric calls return `dropped` receipts with
`instrumentation_disabled`. Applications need no `if VIRGO_API_KEY` or `None`
guards. Explicit blank keys and malformed configuration still raise validation
errors. `trace_export="existing"` retains its credential-free behavior.

Under pytest, `register()` defaults to the same fully inactive behavior before it
reads saved credentials, even when `.virgo/observe.env` exists. Pass
`enabled=True` or set the process environment variable `VIRGO_ENABLED=true` only
for an intentional telemetry integration test. Pass `enabled=False` or set
`VIRGO_ENABLED=false` to force inactive registration outside pytest. The
environment value accepts only `true` or `false`; it is process-only and is not
read from `.virgo/observe.env`.

Reuse your application's existing configuration without adding an environment
variable:

```python
virgo = register(project_name="support-agent", environment=settings.environment)
# Or reuse your existing tracer provider's public resource:
virgo = register(project_name="support-agent", tracer_provider=provider)
# An SDK provider already installed globally is reused automatically:
virgo = register(project_name="support-agent")
```

Resolution uses the first configured label, in this order:

1. `VIRGO_ENVIRONMENT` override.
2. Explicit `environment=...` from application configuration.
3. The supplied or globally registered SDK tracer provider's resource.
4. `OTEL_RESOURCE_ATTRIBUTES`.
5. `APP_ENV`, `VERCEL_TARGET_ENV`, then `VERCEL_ENV`.
6. `NODE_ENV` only when it is `development` or `test`.
7. `unspecified` when no deployment label is available.

Resource lookup prefers `deployment.environment.name`, then
`deployment.environment`, then `platform.environment`. Custom labels and case
are preserved; resource attribute values in the environment variable are
percent-decoded. Blank variables fall through; the selected label must contain
1–80 Unicode characters without control/non-printable characters. An invalid
override fails validation instead of silently using a lower-priority label.
`NODE_ENV=production` describes build mode, so
it does not prove a production deployment. CI flags and cloud host markers do
not classify environments.

Set one override before starting the process, even when application code already
specifies an environment:

```bash
export VIRGO_ENVIRONMENT=local
# Or VIRGO_ENVIRONMENT=prod for production, or VIRGO_ENVIRONMENT=evals for evaluation runs.
```

The label is resolved once at registration for traces, feedback, and metrics.
`local`, `prod`, and `evals` stay exactly as supplied; custom labels remain valid.
Evaluation runs can also use `environment="evals"` in their runner configuration.
CI or `NODE_ENV=test` alone does not imply an evaluation run. `prod` and
`production` remain distinct scopes, so keep your existing label when upgrading.

The same Observe key can send multiple environments to one workspace. The hosted
Virgo endpoint does not identify where your agent runs. Use the Traces environment
filter to inspect deployments separately, and **Columns → Environment** to show
their labels in the table.

**Upgrading:** deployments that previously relied on the implicit `production`
default must set or supply a production label to retain that scope. Historical
traces are not relabeled. Keep delayed feedback and metric producers on the same
environment as the execution they refer to.

```python
from decimal import Decimal
from virgo_observe import MetricSubject, VirgoSubject, VirgoVersions, register

virgo = register(project_name="support-agent", product=True, metrics=True, release="release-7")

with virgo.agent_run(
    "support.resolve_ticket",
    run_ref="run-opaque-123",
    subject=VirgoSubject(account_ref="account-opaque-123"),
    versions=VirgoVersions(agent="agent-v4", prompt="prompt-v7"),
):
    # Run your agent here. Selected supported integrations emit child spans.
    with virgo.span("retrieval.context_pack", kind="RETRIEVER"):
        pass
    answer = "The support policy applies to this ticket."
    with virgo.span("answer.finalize"):
        virgo.set_final_answer(answer, coverage_status="complete")

    saved_execution = virgo.current_trace_ref()
    completed = virgo.metric(
        "resolved_ticket_value",
        observation_id="ticket-123-value",
        value=Decimal("12.34"),
        unit="USD",
        definition_version="resolved-ticket-v1",
        subject=MetricSubject("account", "account-opaque-123"),
        lineage={"producer": "ticket-service"},
    )

# Feedback may arrive later. Persist saved_execution with your own run record
# when the response/callback crosses a process boundary.
feedback = virgo.feedback(
    feedback_id="ticket-123-rating",
    trace=saved_execution,
    rating="down",
    kind="correction",
    correction="The answer should use the selected workspace.",
)

report = virgo.flush_report()
# Inspect report.feedback, report.metrics, and each receipt.status.
```

A saved `TraceRef` contains an external nonzero 32-character lowercase W3C
trace ID and/or your opaque run reference, never a Virgo-internal `ptr_…` ID.
An explicit reference wins over the active execution. Feedback requires one;
metrics may remain deliberately unlinked. Subject references are opaque
application IDs, not pre-hashed Virgo pseudonyms.

Feedback `metadata` accepts only `environment`, `workflow`, `release`,
`model_version`, `configuration_id`, and `provenance`. Values must be non-null
strings, booleans, or finite numbers. Omit missing values and keep arbitrary
event fields in the application's own record. For feedback actually generated
by a simulated user, use `metadata={"provenance": "synthetic_feedback"}`.

`span()` returns a native OpenTelemetry Span. `kind` is an optional AI role
such as `AGENT`, `LLM`, or `TOOL`; `otel_kind=SpanKind.CLIENT` independently
preserves native transport semantics. Generic spans are not inferred to be
tools from parentage. Call `set_final_answer()` inside `agent_run()` when the
application accepts its business answer. It marks the agent root and current
span with `platform.answer.is_final=true`, records answer coverage, and copies
content only when content capture is enabled. The canonical `answer.finalize`
span name receives the same marker. Unmarked child outputs are never promoted
to a trace answer. A `HookDeferredException` after finalization is exported as
`platform.execution.state=deferred`, not as a failed answer; other exceptions
remain errors. `agent_run()`, `get_tracer()`, `flush()`, and
`instrumented_frameworks` remain supported. Typed identity, run, release, and
version arguments own their reserved attribute keys.

## Point observations and real outcome windows

A metric without `window` is one observed event: it requires a non-null
boolean/numeric value, revision 1, and no supersession. Its occurrence is
captured once when called, or set explicitly with an aware `occurred_at`.
It does not fabricate a time window or interpret `False` as a mature failure.
Use an event KPI definition to aggregate these observations; precomputed custom
KPI definitions consume windowed measurements only.

For a completed or censored outcome window, supply `MetricWindow(start, end,
mature_at)` using timezone-aware datetimes. The order is
`start < end <= mature_at`. An observed window must already be mature;
`occurred_at`, if supplied, must equal its end. Supported statuses are
`observed`, `immature`, `proxy`, `right_censored`, `not_achieved`, and
`reversed`. Immature/right-censored values are null.

`value` accepts bool, int, finite float, finite bounded Decimal, or None where
the status permits it. Decimal is serialized as exact decimal text, not a float.
Arbitrary categorical strings are rejected. Subjects have exactly one grain:
account, user, journey, conversation, session, or aggregate (without a ref).
Approved lineage keys are source, source_version, definition_hash, export_id,
and producer.

Corrections retain the logical `observation_id`, increment `revision`, and
set `supersedes_observation_id`. Every revision gets a distinct stable HTTP
idempotency key. Retrying an unchanged revision is a duplicate; changing its
semantic value, timestamp, lineage, subject, release, versions, or execution
claim is a conflict. New revisions never edit the previous record.

## Privacy and integration ownership

**Full content is the default:** prompts, responses, tool inputs/outputs, and
retrieval content. Observe onboarding also configures the server source for
full content. Explicit `capture_content=False` remains available when an
application intentionally needs metadata-only export. When the argument is
omitted, `VIRGO_CAPTURE_CONTENT=true|false` can override the default. Explicit
arguments win; the server's trace-source policy remains authoritative.

In explicit metadata-only mode, the exporter filters its own representation: unapproved attributes,
events (including exception content), status descriptions, link attributes,
and resource/scope attributes are removed. It does not mutate spans delivered
to another exporter. Operational names and approved opaque IDs remain; do not
put prompts, personal data, secrets, or arbitrary content in those fields.
This trace setting does not erase explicitly submitted feedback text, which
follows the feedback source's policy (full content for Observe onboarding).

Explicit metadata-only export retains `vcs.ref.head.revision` and
`platform.commit.sha` only as strings of exactly 40 hexadecimal characters.
Set the emitter commit on the OpenTelemetry `Resource` and provide
`platform.release.id` (for example, via `release=`) for canonical trace source
provenance. Span-only commit attributes do not establish that provenance.
Other source-control fields and arbitrary evaluator or deployment metadata
remain filtered. This does not reconstruct historical provenance.

`product=True` and `metrics=True` independently enable publication using the
same key. Their default URLs use the trace endpoint's origin; the backend
resolves the workspace from the key. Disabled channels are not configured.
Ordinary legacy trace keys do not gain publication access: enable coverage
through Observe setup. Rotating or revoking its trace-source key affects all
selected channels. Existing explicit feedback/outcome tokens remain supported
for older integrations; a shared key is never forwarded to a different origin.

Register before importing functions by value (such as `from litellm import
completion`) and before constructing instrumented clients. Existing local aliases
do not change when a framework module is instrumented. Install only the extras
matching the application's actual frameworks and standalone provider calls.
Omitting `instrumentors` discovers all installed supported integrations; an
explicit list can contain multiple names in any order. Keep existing framework
pins and resolve these extras through the application's existing lock.

Both FastMCP implementations use an instance attachment instead of the
`instrumentors` selection. Install the extra matching the application's existing
import: `virgo-observe[fastmcp]` for standalone FastMCP 3.x/4.x, or
`virgo-observe[mcp-fastmcp]` for FastMCP 1.x bundled with MCP Python SDK
1.28.1–1.x. Full request and response content is enabled by default; credential,
authorization, cookie, token, password, and `_meta` values are always excluded
from the captured copy.

Captured output preserves both text content and structured content, including
large message histories and deeply nested JSON. The adapter does not trim fields,
collections, or serialized bytes to fit the transport. Credential keys are
redacted in both copies, including JSON embedded in text. Cyclic or unserializable
values are explicitly marked; HTTP body budgets still apply to each exported
span, and rejected exports make `flush()` report failure.

For standalone FastMCP:

```python
from fastmcp import FastMCP
from virgo_observe import register

virgo = register(project_name="customer-tools")
mcp = FastMCP("Customer tools")
virgo.instrument_fastmcp(mcp)
```

For the MCP Python SDK's bundled FastMCP, register tools, resources, and prompts
before attaching Virgo:

```python
from mcp.server.fastmcp import FastMCP
from virgo_observe import register

mcp = FastMCP("Customer tools")

@mcp.tool()
def echo(value: str) -> str:
    return value

virgo = register(project_name="customer-tools")
virgo.instrument_fastmcp(mcp)
```

The adapter traces every MCP request and classifies `tools/call`, `prompts/get`,
and `resources/read` as tool, prompt, and retrieval operations. Do not also pass
`fastmcp` in `instrumentors`; both implementations need the concrete server
instance. Stable transport session IDs are preserved when FastMCP provides one.
FastMCP 4's modern protocol and MCP SDK FastMCP do not expose a durable session
identifier at this adapter boundary, so Virgo keeps request correlation without
fabricating a cross-request session.

| Application path | Adapter install for 0.8.0 | Optional `instrumentors` selection |
| --- | --- | --- |
| Claude Agent SDK `0.2.104` | `virgo-observe[claude-agent-sdk]==0.8.0` | `["claude_agent_sdk"]` |
| Codex SDK `openai-codex==0.147.0` | `virgo-observe[codex]==0.8.0` | `["codex"]` |
| Vercel Python `ai==0.5.2` (Python 3.12+) | `virgo-observe[vercel-python]==0.8.0` | `["vercel_python"]` |
| MCP SDK FastMCP `>=1.28.1,<2` | `virgo-observe[mcp-fastmcp]==0.8.0` | `virgo.instrument_fastmcp(mcp)` |
| Standalone FastMCP 3.x/4.x | `virgo-observe[fastmcp]==0.8.0` | `virgo.instrument_fastmcp(mcp)` |
| AutoGen AgentChat | `virgo-observe[autogen]==0.8.0` | `["autogen"]` |
| CrewAI | `virgo-observe[crewai]==0.8.0` | `["crewai"]` |
| LiteLLM | `virgo-observe[litellm]==0.8.0` | `["litellm"]` |
| LlamaIndex | `virgo-observe[llama-index]==0.8.0` | `["llama_index"]` |
| Agno | `virgo-observe[agno]==0.8.0` | `["agno"]` |
| LangGraph | `virgo-observe[langgraph]==0.8.0` | `["langgraph"]` |
| DSPy | `virgo-observe[dspy]==0.8.0` | `["dspy"]` |
| OpenAI Agents SDK | `virgo-observe[openai-agents]==0.8.0` | `["openai_agents"]` |
| Pydantic AI | `virgo-observe[pydantic-ai]==0.8.0` | `["pydantic_ai"]` |
| LangChain | `virgo-observe[langchain]==0.8.0` | `["langchain"]` |
| OpenAI SDK | `virgo-observe[openai]==0.8.0` | `["openai"]` |
| Anthropic SDK | `virgo-observe[anthropic]==0.8.0` | `["anthropic"]` |

Beginning with 0.8.0, an installed release provides its own machine-readable
integration catalog:

```bash
python -m virgo_observe integrations
```

The JSON describes the default full-content setup and lists detection hints,
the install extra, selector, activation method, compatibility package and
supported range, and startup notes. The copied
MCP/UI setup recipe reads this catalog from the release installed in the
application, so a newly published adapter does not require another hard-coded
setup mapping. Detection hints still require checking the application's imports
and lockfile; the runtime `registration_report` confirms actual activation.

### Claude Agent SDK

```python
from virgo_observe import register

register(project_name="support-agent")
```

The single startup call auto-discovers exact `claude-agent-sdk==0.2.104`; the
optional explicit selection is `instrumentors=["claude_agent_sdk"]`. Register
before importing `query` by value or constructing `ClaudeSDKClient` instances.
Virgo wraps the SDK's public async message streams and never consumes messages,
injects hooks, changes options, reads credentials, or owns the Claude Code
subprocess.

Each `query()` call and each `ClaudeSDKClient` response turn emits an AGENT root.
Assistant messages emit LLM children with the SDK-reported model, response ID,
and usage. Tool-use and tool-result blocks emit one correlated TOOL span using
the SDK tool-use ID. A subagent assistant or tool carrying
`parent_tool_use_id` is parented under the corresponding tool span. Stream-event
counts, session ID, final result usage/cost/duration, SDK errors, cancellation,
and incomplete streams remain explicit. Final shutdown marks and ends unfinished
agent/tool spans before the existing bounded exporter shutdown and never closes
the SDK client or subprocess.

`capture_content=False` omits prompt, assistant, result, tool, and exception
content at creation, including with `trace_export="existing"`. Full-content mode
preserves complete fields, redacts known nested credential keys,
and never captures thinking text or signatures. Assistant spans are message
evidence, not claimed provider HTTP requests; their timing is SDK observation
time. Context is correlated inside the Python process, but Virgo does not inject
W3C context into Claude Code's subprocess.

If the OpenInference Claude Agent SDK adapter or Laminar's Claude agent
instrumentation is already active, Virgo reports external ownership and does not
add a second wrapper. Laminar is reported with
`overlapping_laminar_instrumentation`; keep only one Claude integration active
and initialize external telemetry before `register()` so ownership is observable.

### Codex and multiple frameworks

```python
from virgo_observe import register
register(project_name="platform")
```

This single startup call activates installed Pydantic AI, Codex, and the certified
OpenInference OpenAI adapter. An optional selection is
`instrumentors=["pydantic_ai", "codex", "openai"]`. Codex is **openai-codex==0.147.0**,
not OpenAI Agents SDK or Pi. The `codex` extra pins that tested event contract.
One startup source location must execute in every actual API/worker process;
a local call does not configure another deployment.

Observe watches the existing sync SDK request boundary (also used by AsyncCodex)
and typed app-server notifications without consuming the application's stream.
It preserves SDK return values, errors, subprocess isolation, credentials, Codex
configuration, and existing native OTel exporters. Reader-thread notifications
use the captured Python parent context and router/thread/turn IDs. Shutdown ends
remaining observation spans without waiting for model work or closing the SDK.
No manual roots, tool decorators, exporters, or subprocess telemetry settings
are required in the application.

Captured evidence includes turn input/final output, explicitly supplied thread
instructions, model identity, reported usage, tool item IDs, inputs/results,
command output and exit codes, errors, and interruption/cancellation evidence.
Native timestamps/durations are recorded when supplied; OTel span timing measures
SDK observation. Cumulative thread usage becomes a turn delta only with an
observed baseline; resumed/attached unknown baselines remain labeled thread
cumulative. Waiter cancellation does not prove the subprocess was canceled.

Evidence boundaries are explicit on `platform.codex.*` attributes:

- Exact provider requests/responses and individual model-request spans are
  unavailable from the turn event protocol. No synthetic LLM spans are created.
- Parentage is captured at the Python SDK boundary; the adapter does not inject
  W3C context into Codex's subprocess or independent MCP servers. Existing
  native Codex OTel exports continue separately and are not claimed as joined.
- A Codex item ID is the tool correlation ID. A distinct provider call ID,
  hidden runtime instructions, resumed history, tool internals, and file-change
  result text may be unavailable. Missing evidence is flagged, not fabricated.
- Prompts, tool results, terminal input and terminal buffers retain their complete
  content. HTTP body budgets and existing OTel event/attribute limits still apply. Known secret
  keys are redacted from structured content; do not send credentials in prompts,
  terminal output, or free-text tool results. Metadata-only mode omits content.
- Goal-generated turns without an observed SDK `turn/start` and other SDK
  versions are not covered. Verify every exercised execution family separately.

See [verification and remaining gaps](../../docs/research/2026-09-12-observe-codex-implementation.md).

### Vercel Python

```python
from virgo_observe import register

virgo = register(project_name="ember", instrumentors=["vercel_python"])
```

Register once during normal process startup, before executing the agent. Normal
Python exit flushes automatically. The integration configures Vercel's native experimental
OpenTelemetry adapter with Virgo's provider and content policy. Existing
`Agent.run` and SDK tool execution produce their own agent/model/tool spans;
no manual root, tool wrapper, or per-call telemetry flag is needed. Model
messages, system instructions, usage, tool arguments/results and handled errors
are captured. Tool content retains its GenAI fields and is also mapped to the
receiver's input/output fields. `capture_content=False` keeps metadata only.

The extra pins `ai[otel]==0.5.2`; keep the application's existing model-provider
extra/configuration (for example, `ai[openai]`). Only this `ai` version is
certified because its telemetry API is experimental. This is Python `ai`, not
the Node Eve integration. Plain `ai.stream` provides model coverage; it does not
invent an Agent span or observe tools executed outside the SDK. Temporal and
serialized cross-process Vercel span restoration are not certified by this path.

An already-active external provider wrapper blocks Vercel activation with
`overlapping_model_instrumentation`; preserve its ownership and inspect the report.
When native Vercel is selected, additional provider/gateway adapters report the
same overlap reason and are not activated: ai 0.5.2 model spans are not the
ambient parent, so per-call suppression cannot safely identify those duplicates.
Native Vercel agent/model/tool coverage remains active; this optional standalone
provider gap must not block harness onboarding. Existing native
Vercel OTel adapters are preserved and reported `already_active_unverified`,
without adding another one. Other existing exporters on the shared provider
continue receiving spans. Inspect `registration_report` for these cases.

The 0.7.2 release retains the Vercel integration. See the
[live example and release verification](../../docs/research/2026-09-12-observe-automatic-startup-release.md).

Semantic Kernel 1.44.1 uses its native OpenTelemetry spans, so it needs no
adapter extra. Set
`SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE=true`
before importing Semantic Kernel, install base `virgo-observe==0.8.0`, and use
`instrumentors=["semantic_kernel"]`. Semantic Kernel's native tracer resolves
through the global SDK `TracerProvider`; pass that same provider explicitly to
`register()`. Virgo bridges the opted-in prompt/response log events onto the
native model span so its trace exporter applies the same content policy.

Installing an arbitrary OpenInference entry point is not a support guarantee.
Use `auto_instrument=False` for manual tracing.

Automatic selection activates framework, then gateway, then provider layers.
With Pydantic AI, provider and gateway tracers suppress spans when the current
native model span already owns that call. Pydantic/OpenAI concurrent
requests, streaming, standalone Responses and embeddings, and provider calls
inside tools are tested independently. Pydantic AI and Codex no longer disable
standalone provider instrumentation across the registration. Other frameworks
retain their existing provider/gateway overlap guards until per-call suppression
is certified; registration diagnostics identify these optional provider gaps.
Harness onboarding must continue without requiring standalone AI coverage.
Existing external instrumentation is never reconfigured and is reported
`already_active_unverified`, not healthy by inference.

Virgo-owned providers retain all span attributes by default so long message and
tool histories do not evict the model identity or early prompts. Explicit
`OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT` / `OTEL_ATTRIBUTE_COUNT_LIMIT` settings and
application-owned provider limits remain authoritative. Exported spans preserve
OpenTelemetry dropped-attribute, event, and link counters when upstream limits
truncate data; privacy filtering is not counted as upstream data loss.

Inspect `virgo.registration_report` for each integration's distribution version,
status, ownership, and safe reason code. Discovery/activation is not proof of
server delivery. Native automatic metadata-only instrumentation requires patched
Pydantic AI 2.27.1+; earlier versions are skipped because retry content can leak
through their native instrumentation.

Each known integration report now includes `compatibility`: the package whose
version gate is being checked (the OpenInference adapter for adapter gates), its
installed version, supported range, blocked capability, and exact candidate
commands for pip, uv, and Poetry. The standalone
`assess_integration_compatibility(name, installed_version, capture_content=...)`
also returns this information without importing or patching a framework. Missing
version evidence remains `unknown`. Unsupported integrations do not prevent
independent supported integrations from activating.

OpenInference reports also expose `framework_dependencies`: each upstream
instrumentor requirement beside the actual installed framework version (or
`None` when missing). On a dependency conflict, use these requirements to assess
the framework change; reinstalling an already-certified adapter is insufficient.

Candidate upgrades remain `unknown` and are never applied by the SDK. The coding
host must inspect the application's manifest, resolved lock, upstream framework
and adapter requirements, extras and companion pins, release/migration notes,
and affected call sites. It must run focused behavior checks in a disposable
environment before calling an exact application upgrade `verified-compatible`.
Known API/configuration changes are `migration-required`; installation success or
a supported version number is insufficient. For pip requirements projects, update
the existing requirements/constraints and regenerate the existing lock after
upgrade approval. Preserve dependency groups and coordinate `pydantic-ai` with
`pydantic-ai-slim` when both are present.

The reproducible integration matrix uses real SDKs and local fake responses,
including model streaming and native tool/validator retries:

| Layer | Tested minimum | Tested selected current |
| --- | --- | --- |
| Claude Agent SDK native adapter | — | 0.2.104 |
| Vercel Python | `ai==0.5.2`, OpenAI 2.34.0 | `ai==0.5.2`, OpenAI 3.7.0 |
| OpenTelemetry SDK/exporter | 1.42.0 | 1.44.0 (CrewAI: 1.42.0) |
| Pydantic AI slim | 2.27.1 | 2.37.0 |
| OpenAI SDK (OI adapter 0.1.57) | 1.69.0 | 3.7.0 |
| Anthropic SDK (OI adapter 2.1.1) | 1.0.0 | 1.3.0 |
| LangChain Core (OI adapter 0.1.73) | 0.3.50 | 1.6.1 |
| LangChain OpenAI | 0.3.12 | 1.6.0 |
| AutoGen AgentChat (OI adapter 0.1.14) | — | 0.7.5 |
| CrewAI (OI adapter 1.1.15) | — | 1.15.18 |
| LiteLLM (OI adapter 0.1.40) | — | 1.99.0 |
| LlamaIndex Core (OI adapter 4.4.8) | — | 0.14.24 |
| Agno (OI adapter 1.0.6) | — | 3.0.5 |
| LangGraph (LangChain OI adapter 0.1.73) | — | 1.2.11 |
| DSPy (OI adapter 0.1.42) | — | 3.3.1 |
| OpenAI Agents SDK (OI adapter 2.2.0) | — | 0.22.0 |
| Semantic Kernel native OTel | — | 1.44.1 |

Other versions are not individually certified. Install optional integrations
alongside the application's existing dependencies; do not upgrade an application
merely to silence an unsupported registration report.

## Providers, configuration, and lifetime

`register()` uses an explicitly supplied SDK provider, then an existing global
SDK provider, or otherwise a private provider. It never replaces the global
provider. Compatible registrations share one export stream and sender with
reference-counted lifetime; conflicting registrations on that provider fail.
Closing one handle does not shut down another handle or the customer's provider.
Final shutdown releases Virgo-owned framework wrappers only while their ownership
still matches; externally installed or replaced wrappers are left untouched.

In pre-fork servers, create the provider and call `register()` inside each worker
after it starts. Do not share a live Virgo handle or delivery receipt across a
fork. Inherited handles reject operations instead of using stale threads/locks.
Normal Python exit automatically drains traces, feedback and metrics, with one
five-second total deadline across active registrations. This includes ordinary
script completion and uncaught Python exceptions. The SDK starts its lifecycle
workers during registration, so exit cleanup never needs to start new threads.
Explicit `shutdown()` and context-manager cleanup remain supported and do not
duplicate exports. Existing application-owned providers are not shut down.

Use explicit `flush_report()` before a serverless runtime freezes, or `shutdown()`
in an application's existing graceful-stop lifecycle when it does not reach
normal interpreter exit. `SIGKILL`, `os._exit()` and crashes cannot run exit hooks.
Process exit is not a remote delivery receipt.

`trace_export="existing"` requires an existing SDK provider, needs no Virgo
trace key, and attaches no Virgo exporter. The application's exporter and privacy
policy own that path; Virgo's export-local content filter does not apply to it.
The independent feedback/metric channels still work.

When those channels use an Observe key, including one supplied through explicit
channel token arguments or environment variables, an existing exporter must carry
the environment contract on **every** span's resource, including children exported
before their root. Configure it before creating the provider:

```python
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider

provider = TracerProvider(resource=Resource.create({
    "deployment.environment.name": "local",
    "platform.observe.environment_contract": "observe-runtime-environment-v1",
}))
# Attach your existing exporter to this provider before registration.
virgo = register(
    project_name="support-agent",
    tracer_provider=provider,
    trace_export="existing",
    environment="local",
    product=True,
    metrics=True,
)
```

Registration rejects a missing contract or mismatched environment before starting
delivery. It never mutates the existing provider or another exporter's resources.
Trace-only existing-export registrations keep their previous behavior.

Explicit arguments take precedence over the channel environment variables.
Trace compatibility also accepts `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`,
`OTEL_EXPORTER_OTLP_TRACES_HEADERS`/`OTEL_EXPORTER_OTLP_HEADERS`, and
`OTEL_SERVICE_NAME`. `VIRGO_ENDPOINT`, `VIRGO_ENVIRONMENT`, and
`VIRGO_RELEASE` override hosted defaults. The project display name is never
used to guess a workspace publication URL. Missing channels are permitted at
registration, but publishing to one raises a configuration error.

Each record channel has its own finite in-memory queue and worker (default 512
pending events each). Payloads, occurrence times, IDs, and operational W3C
headers are captured before enqueueing. Network calls suppress recursive HTTP
instrumentation. At most three attempts retry transport errors, 408, 429, and
5xx responses with bounded jitter/backoff and Retry-After. Other HTTP failures
are terminal; redirects never forward credentials.

Receipts expose queued, accepted, rejected, conflict, failed, or dropped states.
Queue overflow is visible as dropped, not silently successful. A 202 means
accepted at ingress, not normalized, linked, or successfully analyzed.
`flush_report()` reports cumulative failures and pending records;
`flush()` is its boolean compatibility view. For Virgo-owned trace export,
`traces_flushed` requires both a drained processor and successful exporter results.
A rejected export remains a failure for that registration: a later empty queue
cannot recover missing spans. Existing-provider mode reports the external
provider's force-flush result only.

Default trace exports use gzip and size-aware batches with a 3.5 MB protobuf
target. Content is never truncated to meet that target. For Platform's
`/v1/lab-ingest/otel/v1/traces` endpoint, an individual oversized span uses
checksummed, encrypted durable parts; only the completed ingest receipt counts
as delivery. Partial uploads never appear as traces. Part and commit retries
share `transfer_timeout_seconds` (30 seconds by default), while each request
uses at most `export_timeout_seconds` (5 seconds by default). A multipart
transfer needs several network round trips plus server assembly and ingestion;
it does not share the single-request budget. The API must support transfer protocol 1
before using this SDK change; deployment and SDK publication are separate steps.
Custom exporters and other OTLP endpoints keep their existing delivery behavior.
Source/framework limits explicitly configured by the application still apply.
Inspect `platform.observe.delivery.events` for channel `traces` and
`trace_export_failed` warnings when delivery fails.

Flush and shutdown share a total caller deadline across channels (30 seconds by
default). Automatic exit cleanup uses the configured transfer budget for
Platform trace delivery; other exporters retain their five-second exit bound.
Pass a shorter `timeout_seconds`
when the application has a tighter suspension deadline; a timed-out flush
returns false, and a later flush can confirm an export that is still running.
An exporter
or in-flight HTTP call can finish after the caller's deadline; Python cannot
safely cancel arbitrary third-party I/O. Shutdown marks undelivered receipts
failed and closes owned resources. This queue is **not crash-durable**:
persist important application events in your own outbox and reuse their IDs
when retrying after a process restart. Explicit shutdown remains available for
application-managed teardown; ordinary scripts use automatic exit cleanup.

## Server contract and verification

Point metrics, explicit observation/release/version fields, and durable
privacy-aware correlations require the accompanying Platform server changes
and forward migration 0197. Upgrade via `platform-migrate`; never use an SDK
client-side fallback against an older window-only endpoint.

Exact links require the same tenant, workspace, environment, and permitted
pseudonym scope. Multiple candidates and contradictory references abstain.
The Observe key selects the workspace; `environment` identifies the deployment
within it and resolves as described above. Version 0.6.2 requires the accompanying
server environment support. Deploy that server before upgrading the SDK.
SDK 0.6.2 declares `platform.observe.environment_contract=observe-runtime-environment-v1`
on exported resources. Only Observe traces with that exact contract use their
runtime environment; legacy Observe exports and payloads without an environment
retain their shared source/workspace default. This keeps older clients' links
working during an API-first rollout. Generic OTLP sources retain their existing
resource-environment behavior.
The API accepts `metadata.environment` on feedback and top-level `environment`
on metrics. Observe keys permit these deployment labels within their workspace;
legacy channel credentials require the workspace environment. Labels must be
nonblank strings of at most 80 characters. Normal Unicode labels are preserved;
control, format, private-use, unassigned, and surrogate characters are rejected
before export or ingestion because they cannot identify a correlation scope.
Late links live in a separate projection, not edits to immutable outcome rows.
Pending claims receive bounded retries and a final expiry check; missing or
sampled-out traces do not prevent observation acceptance.

From the Platform repository root:

```bash
uv run ruff format packages/virgo-observe-python
uv run ruff check packages/virgo-observe-python
uv run mypy packages/virgo-observe-python/src
uv run pytest packages/virgo-observe-python/tests
uv run python packages/virgo-observe-python/scripts/check_framework_matrix.py
uv build packages/virgo-observe-python
uv run python scripts/verify_virgo_observe_distribution.py
```

See `docs/virgo/instrumentation/python.md`, `docs/observability.md`, and
`docs/virgo/instrumentation/python-release.md` for server inspection and the
separately authorized publication procedure.

## License

The `virgo-observe` Python SDK is licensed under the
[Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).
The full license is included in this package's `LICENSE` file and its wheel
and source distributions.

This license applies only to `packages/virgo-observe-python` within the
Platform repository. It does not license the rest of the Platform repository
or Virgo's hosted services. Third-party dependencies retain their own licenses.
