Observability
TelemetryHooks instruments every agent run with OpenTelemetry spans. Configure any OTLP-compatible backend — Jaeger, Honeycomb, Grafana Tempo, Datadog, or the console — then pass TelemetryHooks() to AgentConfig.
Quick start
Section titled “Quick start”from cyclops import Agent, AgentConfig, TelemetryHooks
agent = Agent(AgentConfig(model="groq/llama-3.1-8b-instant", hooks=TelemetryHooks.console()))agent.run("Summarise the Pythagorean theorem.")TelemetryHooks.console() sets up a TracerProvider and ConsoleSpanExporter automatically — no boilerplate required. Spans are flushed automatically at the end of each run.
Span hierarchy
Section titled “Span hierarchy”Each agent.run() call produces a root span with nested children:
agent.run├── llm.completion├── tool.<name>├── llm.completion└── tool.<name>Span attributes
Section titled “Span attributes”| Span | Attribute | Description |
|---|---|---|
agent.run | agent.input.length | Character length of the input message |
agent.run | agent.output.length | Character length of the final response |
llm.completion | llm.message_count | Number of messages sent to the LLM |
llm.completion | llm.model | Model identifier returned by the API |
llm.completion | llm.prompt_tokens | Prompt token count |
llm.completion | llm.completion_tokens | Completion token count |
llm.completion | llm.total_tokens | Total token count |
llm.completion | llm.latency_ms | Round-trip latency in milliseconds |
tool.<name> | tool.name | Tool name |
tool.<name> | tool.args_count | Number of arguments passed |
tool.<name> | tool.result.length | Character length of the tool result |
| Any | error | true when the span represents a failure |
| Any | error.message | Exception message |
| Any | error.type | Exception class name |
Send to Jaeger (OTLP)
Section titled “Send to Jaeger (OTLP)”from cyclops import Agent, AgentConfig, TelemetryHooks
hooks = TelemetryHooks.otlp("http://localhost:4317")agent = Agent(AgentConfig(model="groq/llama-3.1-8b-instant", hooks=hooks))agent.run("Summarise the Pythagorean theorem.")hooks.flush()Install the OTLP exporter first:
uv add opentelemetry-exporter-otlp-proto-grpcStart Jaeger locally:
docker run -d --name jaeger \ -p 16686:16686 \ -p 4317:4317 \ jaegertracing/all-in-one:latestThen open http://localhost:16686 and search for service cyclops.
Known limitation
Section titled “Known limitation”on_run_end is not called for stream() / astream(), so the root agent.run span is not closed for streaming runs. Use run() or arun() if you need complete traces.
Full example
Section titled “Full example”"""OpenTelemetry observability example.
Demonstrates wiring TelemetryHooks to an agent and emitting spans to the console.Use TelemetryHooks.otlp("http://localhost:4317") to send to Jaeger, Honeycomb,Grafana Tempo, Datadog, or any OTLP-compatible backend.
Span hierarchy per agent.run(): agent.run ├── llm.completion (attributes: model, tokens, latency_ms) ├── tool.<name> (attributes: name, args_count, result.length) └── ..."""
from cyclops import Agent, AgentConfig, TelemetryHooksfrom cyclops.toolkit import tool
MODEL = "ollama/qwen3:4b"
# Alternatives:# MODEL = "groq/llama-3.1-8b-instant" # GROQ_API_KEY# MODEL = "anthropic/claude-haiku-4-5-20251001" # ANTHROPIC_API_KEY# MODEL = "gpt-4o-mini" # OPENAI_API_KEY
# ---------------------------------------------------------------------------# 2. Tools# ---------------------------------------------------------------------------
@tooldef get_weather(location: str) -> str: """Get the current weather for a location.""" return f"Sunny, 22C in {location}"
@tooldef calculate(expression: str) -> str: """Evaluate a simple arithmetic expression.""" try: return str(eval(expression)) # noqa: S307 (demo only) except Exception as exc: return f"Error: {exc}"
# ---------------------------------------------------------------------------# 3. Run agent with TelemetryHooks# ---------------------------------------------------------------------------
def main() -> None: # TelemetryHooks.console() sets up a TracerProvider + ConsoleSpanExporter in one call. # Swap for TelemetryHooks.otlp("http://localhost:4317") to send to Jaeger/Tempo/etc. hooks = TelemetryHooks.console()
agent = Agent( AgentConfig(model=MODEL, hooks=hooks), tools=[get_weather, calculate], )
print("Running agent...\n") agent.run("What is the weather in Tokyo, and what is 42 multiplied by 17?") print("\nDone. Spans printed above.")
if __name__ == "__main__": main()