Subchapter 155.4
references/2a-instrumentation.mdMarkdown7 KBView on GitHub
wrapFor the full
wrap()API reference, seewrap-api.md.
Goal: Add wrap() calls at data boundaries so the eval harness can (1) inject controlled inputs in place of real external dependencies, and (2) capture outputs for scoring.
Starting from LLM call sites, trace backwards and forwards through the code to find:
You do not need to wrap LLM call arguments or responses — those are already captured by OpenInference auto-instrumentation.
For each data point found, add a wrap() call in the application code:
import pixie
# External dependency data — function form (prevents the real call in eval mode)
profile = pixie.wrap(db.get_profile, purpose="input", name="customer_profile",
description="Customer profile fetched from database")(user_id)
# External dependency data — function form (prevents the real call in eval mode)
history = pixie.wrap(redis.get_history, purpose="input", name="conversation_history",
description="Conversation history from Redis")(session_id)
# App output — what the user receives
response = pixie.wrap(response_text, purpose="output", name="response",
description="The assistant's response to the user")
# Intermediate state — internal decision relevant to evaluation
selected_agent = pixie.wrap(selected_agent, purpose="state", name="routing_decision",
description="Which agent was selected to handle this request")# Value form: wrap a data value (result already computed)
profile = pixie.wrap(db.get_profile(user_id), purpose="input", name="customer_profile")
# Function form: wrap the callable — in eval mode the original function is
# NOT called; the registry value is returned instead.
profile = pixie.wrap(db.get_profile, purpose="input", name="customer_profile")(user_id)CRITICAL: Always use function form for purpose="input" wraps on external calls — HTTP requests, database queries, API calls, file reads, cache lookups. Function form prevents the real call from executing in eval mode, so the dataset value is returned directly without making a live network request or database query. Value form still executes the real call first and only replaces the result afterwards — this wastes time, creates flaky tests, and makes evals dependent on external service availability.
The only case where value form is acceptable for purpose="input" is when the wrapped value is a local computation (no I/O, no side effects) that is cheap to recompute.
lower_snake_case for names.wrap() is purely additive, returns the same type.Place input wraps at the boundary where external data enters the app, not at intermediate processing stages. In a pipeline architecture (fetch → process → extract → format):
wrap(fetch_page, purpose="input", name="fetched_page")(url) using function form at the HTTP fetch boundary — in eval mode, the fetch is skipped entirely and the dataset value is returned; in trace mode, the real fetch runs and the result is captured.wrap(html_content, purpose="input", name="fetched_page") using value form — the HTTP fetch still runs in eval mode (wasting time and creating flaky tests), and only the result is replaced afterwards.wrap(processed_chunks, purpose="input", name="chunks") after parsing — eval mode bypasses parsing and chunking entirely.Principle: wrap(purpose="input") replaces the minimum external dependency while exercising the maximum internal logic. Push the boundary as far upstream as possible. Always use function form for input wraps on external calls — this prevents the real call from executing in eval mode.
Track downstream from the LLM response to find where data leaves the app — sent to the user, written to storage, rendered in UI, or passed to an external system. Wrap at that exit boundary.
llm_span entries.# Final response after the app's formatting pipeline
response = pixie.wrap(formatted_response, purpose="output", name="response",
description="Final response sent to the user")
# Side-effect output — data written to external storage
pixie.wrap(saved_record, purpose="output", name="saved_summary",
description="Summary record saved to the database")Principle: output wraps are observation-only — they capture what the app produced so evaluators can score it. They are never mocked or injected during eval runs.
Some eval criteria need to judge the app’s internal reasoning — not just what went in or came out, but how the app made decisions. Wrap internal state when an eval criterion requires it and the data isn’t visible in inputs or outputs.
Common examples:
# Agent routing decision
selected_agent = pixie.wrap(selected_agent, purpose="state", name="routing_decision",
description="Which agent was selected to handle this request")
# Retrieved context fed to LLM
pixie.wrap(retrieved_chunks, purpose="state", name="retrieved_context",
description="Document chunks retrieved by RAG before LLM call")Principle: only wrap state that an eval criterion actually needs. Don’t wrap every variable — state wraps are for internal data that evaluators must see but that doesn’t appear in the app’s inputs or outputs.
After adding all wrap() calls, go through each eval criterion from pixie_qa/02-eval-criteria.md and verify:
input or entry wrap.output wrap.state wrap.If a criterion needs data that isn’t captured, add the wrap now — don’t defer.
Modified application source files with wrap() calls at data boundaries.