Observations
Record each model call your agent makes as an observation: the JSON format and the span attribute that carries it.
An observation records one model call your agent makes: everything the model was given and what it produced. Judgment groups observations into conversations and shows them, exactly as the model saw them, in the Conversation view. Supported integrations record observations automatically. For any other model call, send the observation yourself.
Send an observation
Set the observation on the model call’s span. It travels as JSON text under judgment.conversation.observation. One span holds one observation; setting it again replaces it.
Tracer.set_conversation_observation takes a typed ConversationObservation, so your type checker flags fields the format doesn’t define before Judgment rejects them.
from judgeval import Tracer
from judgeval.trace import ConversationMessage, ConversationObservation
@Tracer.observe(span_type="llm", span_name="support_reply")
def reply(history: list[ConversationMessage]) -> str:
response = my_model.generate(history)
observation: ConversationObservation = {
"model": "my-model-v2",
"input": [
{"type": "message", "role": "system", "content": [{"type": "text", "text": "You help customers with orders."}]},
*history,
],
"output": [
{"type": "message", "role": "assistant", "content": [{"type": "text", "text": response.text}]},
],
"finish_reason": "stop",
}
Tracer.set_conversation_observation(observation)
return response.textTracer.setConversationObservation takes a typed ConversationObservation, so the compiler flags fields the format doesn’t define before Judgment rejects them.
import { Tracer, type ConversationMessage, type ConversationObservation } from "judgeval";
const reply = Tracer.observe(async (history: ConversationMessage[]) => {
const response = await myModel.generate(history);
const observation: ConversationObservation = {
model: "my-model-v2",
input: [
{ type: "message", role: "system", content: [{ type: "text", text: "You help customers with orders." }] },
...history,
],
output: [
{ type: "message", role: "assistant", content: [{ type: "text", text: response.text }] },
],
finish_reason: "stop",
};
Tracer.setConversationObservation(observation);
return response.text;
}, { spanType: "llm", spanName: "support_reply" });import json
from opentelemetry import trace
span = trace.get_current_span()
span.set_attribute("judgment.conversation.observation", json.dumps(observation))Observation
| Field | Type | Meaning |
|---|---|---|
input |
Item list, required | Everything the model was given, in order. |
output |
Item list | What the model produced, when recorded. |
conversation_id |
String | The thread of context the call belongs to. Defaults to the span’s judgment.session_id. One of the two is required. |
customer_id |
String | The customer the call ran for. Defaults to the span’s judgment.customer_id. |
user_id |
String | The user the call ran for. Defaults to the span’s judgment.customer_user_id. |
tools |
Tool list | The tools the model could call: name, and optional description, parameters (JSON Schema of the arguments), and metadata. |
model |
String | The model that ran the call. |
finish_reason |
String | Why the model stopped, as the provider reported it. |
usage |
Usage | What the call consumed. Defaults to the span’s judgment.usage.* attributes. |
error |
Error | A recorded model-call failure. |
metadata |
Object of JSON values | Any other facts about this call. |
Set conversation_id (or the session ID) to the same value on every call that continues the same context. See Session guidance.
Items
input and output are flat, ordered lists of items. An assistant turn with text and two tool calls is one message item followed by two tool_call items.
type |
Fields |
|---|---|
message |
role (system, user, or assistant), content (part list) |
tool_call |
name, arguments (any JSON), optional call_id |
tool_result |
output (part list), optional call_id (the call it answers), optional error |
reasoning |
text |
custom |
kind (a name such as acme:handoff), data (any JSON) |
Parts
type |
Fields |
|---|---|
text |
text |
image, audio, file |
source: {"type": "url", "url": ...} or {"type": "base64", "data": ...}; optional media_type, such as image/png |
json |
value (any JSON) |
custom |
kind, data (any JSON) |
Use custom items and parts for data observations do not define yet.
Usage
Every field is optional. Leave out what you don’t know; don’t send zero.
| Field | Meaning |
|---|---|
non_cached_input_tokens |
Input tokens charged at the normal rate. |
non_cached_input_image_tokens |
Image input tokens charged at the normal rate. |
cache_read_input_tokens |
Input tokens read from the prompt cache. |
cache_creation_input_tokens |
Input tokens written to the prompt cache. |
output_tokens |
Generated tokens, excluding reasoning. |
output_image_tokens |
Generated image tokens. |
reasoning_tokens |
Tokens spent reasoning. |
total_cost_usd |
The call’s cost in USD. |
Each field means the same as the matching judgment.usage.* attribute in Attribute keys.
Errors
error on the observation records a failed model call. error on a tool_result records a failed tool. Both take optional type, message, and stacktrace; {} records a failure without details. Judgment never infers a failure from span status or a missing output.
Rules
- Send the whole window.
inputholds every item the model was given, including history from earlier calls. Judgment stores each distinct item once per conversation, so resent history costs little storage. It still counts toward the span’s size. - Keep per-call facts off items. An item must be exactly what is resent on later calls. Finish reason, usage, and latency belong on the observation.
- No unknown fields. Every object is closed. A field this page does not define rejects the whole observation.
Validation and limits
An observation that does not fit this format is not stored. Judgment sets judgment.conversation.warnings on its span, listing each problem by field path. The span and its trace are stored as usual.
| Limit | Value |
|---|---|
| IDs, names, kinds, models, finish reasons, media types | 1 to 512 characters |
| Span attributes, including the observation | 8 MiB per span. See Ingest contract. |