Traces | Lilypad
MirascopeLilypad

Traces

If you haven't already, we recommend reading Spans first.

None of the tooling below requires LLMs. It is, however, very handy when working with them.

As mentioned in the section on nested spans, traces are really just collections of spans.

In the context of observability, a "trace" often refers to a record of events in a system's execution. Generally this means capturing the inputs, outputs, and additional metadata for each function or module in your system's execution flow.

Lilypad makes this simple and easy with the trace decorator:

import lilypad

lilypad.configure()

@lilypad.trace()  
def child(text: str) -> str:
    return "Child Finished!"

@lilypad.trace()  
def parent(text: str) -> str:
    output = child("I'm the child!")
    print(output)
    return "Parent Finished!"
    
output = parent("I'm' the parent!")
print(output)
# > Child Finished!
# > Parent Finished!

Basic Trace

Custom Name

By default, the trace decorator will use the decorated function's name for the trace. Sometimes it makes more sense to use a custom (more readable) name:

import lilypad

lilypad.configure()

@lilypad.trace(name="Answer Question")  
def answer_question(question: str) -> str:
    return "The capital of France is Paris."
    
answer = answer_question("What is the capital of France?")
print(answer)
# > The capital of France is Paris.

Custom Trace Name

Updating Trace Metadata

The trace decorator captures information such as inputs/outputs by default, but often you'll want to log additional information or metadata as part of that function's span (and not a sub-span).

We've made this possible in a type-safe way with a special trace_ctx reserved argument name.

import lilypad

lilypad.configure()

@lilypad.trace(name="Answer Question")
def answer_question(trace_ctx: lilypad.Span, question: str) -> str:  
    trace_ctx.log("I'm the span for Answer Question.")  
    return "The capital of France is Paris."
    
answer = answer_question("What is the capital of France?")
print(answer)
# > The capital of France is Paris.

If a trace-decorated functions has trace_ctx: lilypad.Span as it's first argument, the decorator will inject the span into the argument so that you can access it directly inside the function.

The resulting decorated function's call signature will then be updated such that trace_ctx is excluded (since trace_ctx will be supplied by the decorator, not the user).

Above, answer_question only expects question as an input — and your editor knows this.

Trace With A Log

Tracing LLM Calls

Since LLMs are non-deterministic, we recommend versioning any functions that use them.

Since Lilypad can create spans for LLM API calls automatically, simple calling the API inside of a trace-decorated function will nest that span inside of the parent function:

from google.genai import Client
import lilypad

lilypad.configure(auto_llm=True)  
client = Client()


@lilypad.trace(name="Answer Question")
def answer_question(question: str) -> str | None:
    response = client.models.generate_content(
        model="gemini-2.0-flash-001",
        contents=f"Answer this question: {question}",
    )
    return response.text


response = answer_question("What is the capital of France?")
print(response)

Trace With Auto LLM Span