# Using Emitters

[](){ #using-emitter }

While returning a single float for the final reward is sufficient for many algorithm-agent combinations, some advanced scenarios require richer feedback. For instance, an algorithm might learn more effectively if it receives intermediate rewards throughout a multi-step task, or if the agent needs to emit additional spans for debugging or analysis.

Agent-lightning provides an **emitter** module for recording custom spans inside your agent logic. Just as [Tracer][agentlightning.Tracer] automatically instruments common operations (for example, LLM calls), each emitter helper sends a [Span][agentlightning.Span] that captures Agent-lightning-specific work so downstream algorithms can query it later. See [Working with Traces](./traces.md) for more details.

For multi-step routines such as function calls, tools, or adapters, wrap code with [`operation`][agentlightning.operation] — either as a decorator or a context manager — to capture inputs, outputs, and metadata on a dedicated [`operation`][agentlightning.operation] span. This makes it easier to correlate downstream annotations (like rewards or messages) with the higher-level work that produced them.

You can find the emitter functions in [`agentlightning.emitter`](../reference/agent.md).

## Emitting Rewards, Messages, and More

Here are the primary emitter functions:

* [`emit_reward(value: float)`][agentlightning.emit_reward]: Records an intermediate/final reward, which is a convenient wrapper of [`emit_annotation`][agentlightning.emit_annotation].
* [`emit_annotation(attributes: Dict[str, Any])`][agentlightning.emit_annotation]: Records arbitrary metadata as a span.
* [`emit_message(message: str)`][agentlightning.emit_message]: Records a simple log message as a span.
* [`emit_exception(exception: BaseException)`][agentlightning.emit_exception]: Records a Python exception, including its type, message, and stack trace.
* [`emit_object(obj: Any)`][agentlightning.emit_object]: Records any JSON-serializable object, perfect for structured data.

Let's first see an example of an agent using these emitters to provide detailed feedback.

```python
import agentlightning as agl

@agl.rollout
def multi_step_agent(task: dict, prompt_template: PromptTemplate) -> float:
    try:
        # Step 1: Initial planning
        agl.emit_message("Starting planning phase.")
        plan = generate_plan(task, prompt_template)
        agl.emit_object({"plan_steps": len(plan), "first_step": plan[0]})

        # Award a small reward for a valid plan
        plan_reward = grade_plan(plan)
        agl.emit_reward(plan_reward)

        # Step 2: Execute the plan
        agl.emit_message(f"Executing {len(plan)}-step plan.")
        execution_result = execute_plan(plan)

        # Step 3: Final evaluation
        final_reward = custom_grade_final_result(execution_result, task["expected_output"])

        # The return value is treated as the final reward for the rollout
        return final_reward

    except ValueError as e:
        # Record the specific error and return a failure reward
        agl.emit_exception(e)
        return 0.0
```

Each helper accepts nested `attributes` (or keyword arguments for [`operation`][agentlightning.operation]) and automatically flattens/sanitizes them into dotted OpenTelemetry keys. This means you can pass ordinary dictionaries/lists without pre-processing and still get consistent attribute names such as `meta.any_attribute` across all emitter operations. Agent-lightning does not restrict the attributes you supply, but it is best to consult [OpenTelemetry's semantic conventions](https://opentelemetry.io/docs/specs/semconv/) for recommended names. Agent-lightning also defines [specific semconv](../reference/semconv.md) for its own use cases. The pattern looks like this:

```python
from opentelemetry.semconv.attributes import server_attributes
from agentlightning import emit_object

emit_object({
    "name": "John Doe",
    "age": 30,
    "email": "john.doe@example.com",
}, attributes={
    server_attributes.SERVER_ADDRESS: "127.0.0.1",
    server_attributes.SERVER_PORT: 8080,
})
```

Running the above code sends the following span to the backend if you have a tracer active:

```text
Span(
    name='agentlightning.object',
    attributes={
        'agentlightning.object.type': 'dict',
        'agentlightning.object.json': '{"name": "John Doe", "age": 30, "email": "john.doe@example.com"}',
        'server.address': '127.0.0.1',
        'server.port': 8080
    }
)
```

!!! tip

    If you don't have a tracer active, the above code will raise the following error:

    ```text
    RuntimeError: No active tracer found. Cannot emit object span.
    ```

    By default, emitter helpers delegate to the active tracer to create and export spans (specifically via [`Tracer.create_span`][agentlightning.Tracer.create_span]). If you want to emit spans without an active tracer, set `propagate=False` to keep the span local — a useful option for offline tests. The default `True` streams spans through the active tracer/exporters.

When working with [agentlightning.semconv](../reference/semconv.md), you typically use utilities such as [`make_tag_attributes`][agentlightning.utils.otel.make_tag_attributes] and [`make_link_attributes`][agentlightning.utils.otel.make_link_attributes] to build the attributes dictionary. For example:

```python
from agentlightning.utils.otel import make_tag_attributes

emit_annotation(make_tag_attributes(["tool", "calculator", "fast", "good"]))
```

The above code will send a span with the following attributes to the backend:

```json
{
    "agentlightning.tag.0": "tool",
    "agentlightning.tag.1": "calculator",
    "agentlightning.tag.2": "fast",
    "agentlightning.tag.3": "good"
}
```

A counterpart utility function [`extract_tags_from_attributes`][agentlightning.utils.otel.extract_tags_from_attributes] is also available to extract the tags from the attributes dictionary.

## Operations

The [`operation`][agentlightning.operation] helper tracks logical units of work within your agent, capturing inputs, outputs, timing, and success/failure status. Unlike point-in-time emitters, operations create a span representing a time interval. Use operations for tool calls, multi-step workflows, debugging, and performance monitoring. [`operation`][agentlightning.operation] works as either a decorator or a context manager.

The decorator automatically captures function arguments as inputs and the return value as output:

```python
import agentlightning as agl

@agl.operation
def search_documents(query: str, max_results: int = 10) -> list[dict]:
    results = perform_search(query, max_results)
    return results

@agl.operation(category="tool", priority="high")
def execute_calculation(expression: str) -> float:
    return eval_safely(expression)
```

The example above emits a span with `{"category": "tool", "priority": "high"}` attributes. It also records the function input and output via [OPERATION_INPUT][agentlightning.semconv.LightningSpanAttributes.OPERATION_INPUT] and [OPERATION_OUTPUT][agentlightning.semconv.LightningSpanAttributes.OPERATION_OUTPUT]. It works with async functions too:

```python
@agl.operation
async def async_api_call(endpoint: str, payload: dict) -> dict:
    response = await http_client.post(endpoint, json=payload)
    return response.json()
```

Override the operation name if needed:

```python
@agl.operation(name="custom-name")
def any_weird_name_i_dont_want():
    pass
```

For more control, [`operation`][agentlightning.operation] can also be used as a context manager to explicitly record inputs and outputs:

```python
with agl.operation(tool_name="web_search") as op:
    op.set_input(query="latest AI research", filters={"date": "2024"})
    results = search_web("latest AI research", {"date": "2024"})
    op.set_output({"result_count": len(results), "top_result": results[0]})
```

The `propagate=False` flag also applies to [`operation`][agentlightning.operation] when you want to keep operations local without requiring an active tracer:

```python
@agl.operation(propagate=False)
def local_test():
    return "Not sent to backend"
```

## Linking to Other Spans

Sometimes a span should explicitly point back to another span that produced the input it is working on (for example, linking a reward annotation to the [`agentlightning.operation`][agentlightning.operation] span that generated a response). Agent-lightning encodes these relationships through flattened link attributes. The helper [`make_link_attributes`][agentlightning.utils.otel.make_link_attributes] converts a dictionary of keys such as `trace_id`, `span_id`, or any custom attribute into the `"agentlightning.link.*"` ([LightningSpanAttributes.LINK][agentlightning.semconv.LightningSpanAttributes.LINK]) fields expected by the backend. Later, [`query_linked_spans`][agentlightning.utils.otel.query_linked_spans] can recover the original span(s) from those link descriptors.

```python
import opentelemetry.trace as trace_api
from agentlightning import emit_annotation, operation
from agentlightning.utils.otel import make_link_attributes, make_tag_attributes

with operation(conversation_id="chat-42") as op:
    # ... perform the work ...
    link_attrs = make_link_attributes({
        "conversation_id": "chat-42",
    })

    emit_annotation(
        {
            **link_attrs,
            **make_tag_attributes(["reward", "good"]),
        }
    )
```

When analyzing in adapters, pass the extracted link models to [`query_linked_spans`][agentlightning.utils.otel.query_linked_spans] to retrieve the matching span(s):

```python
from agentlightning.utils.otel import extract_links_from_attributes, query_linked_spans

annotation_span = ...  # Span from your trace store
operation_spans = [...]  # list of spans you want to search

link_models = extract_links_from_attributes(annotation_span.attributes)
matches = query_linked_spans(operation_spans, link_models)
assert matches  # Contains the original operation span
```

!!! tip "Correlating Rewards with LLM Requests"

    [Tracer](./traces.md) instruments each request/response as its own span. You can link to the [`gen_ai.response.id`](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-events/) attribute, which comes from the LLM response ID.

    ```python
    from agentlightning import emit_reward
    from agentlightning.utils.otel import make_link_attributes

    result = call_llm(prompt)
    reward_links = make_link_attributes({"gen_ai.response.id": result.id})
    emit_reward(0.9, attributes=reward_links)
    ```

    Later, use the same `gen_ai.response.id` key inside `query_linked_spans` to find the reward(s) that reference that specific LLM request span.
