Skip to main content

Lightweight observability SDK for AI agents — non-blocking run tracking with batching and retries

Project description

agenttrace

Lightweight observability SDK for AI agents. Track runs, steps, tokens, cost, and latency — with zero blocking overhead.

with agenttrace.track_run("my-agent", model="llama-3.3-70b") as run:
    result = llm.call(prompt)
    run.add_step("llm_response", input=prompt, output=result, tokens=150, latency=320)

Completed runs are flushed to your AgentTrace dashboard in the background — your agent never waits on a network call.


Features

  • Async-safe — per-run objects on the call stack, no global mutable state
  • Non-blocking — background worker thread + queue.Queue; agent execution is never delayed
  • Reliable — exponential backoff retries with jitter (4 attempts by default)
  • Batching — configurable batch size and flush interval
  • Zero dependencies — stdlib only (urllib, queue, threading, contextlib)
  • Sync + async — context managers and decorator for both sync and async code

Install

pip install agenttrace

Quick start

Set your credentials in .env (or export as environment variables):

AGENTTRACE_API_KEY="at_xxxxxxxxxxxxxxxxxxxx"
AGENTTRACE_URL="https://your-dashboard.com"   # default: http://localhost:3001
AGENTTRACE_SERVICE="my-agent"                 # default: agent
from dotenv import load_dotenv
load_dotenv()

import agenttrace

with agenttrace.track_run("my-agent") as run:
    # ... your agent logic ...
    run.add_step("llm_response", input="Hello", output="Hi there", tokens=20, latency=310)

# For scripts: flush before process exit
agenttrace.flush()

Usage

Sync — context manager

import time
import agenttrace

with agenttrace.track_run(
    "hotel-search-agent",
    model="llama-3.3-70b",
    user_id="user_123",
    tags=["prod", "search"],
) as run:

    # Track an LLM call
    t0 = time.time_ns()
    response = groq_client.chat.completions.create(model=..., messages=...)
    run.add_step(
        "llm_response",
        input=messages[-1]["content"],
        output=response.choices[0].message.content,
        tokens=response.usage.total_tokens,
        latency=(time.time_ns() - t0) // 1_000_000,   # ns → ms
    )

    # Track a tool call
    t1 = time.time_ns()
    results = web_search(query)
    run.add_step(
        "tool_call",
        input=query,
        output=results[:2000],
        latency=(time.time_ns() - t1) // 1_000_000,
    )

The run is enqueued the moment the with block exits — whether it succeeded or raised an exception.

Async — context manager

Identical API, works inside async def functions and coroutines:

import agenttrace

async def handle_request(query: str) -> str:
    async with agenttrace.async_track_run("my-agent", model="gpt-4o") as run:
        result = await llm.acall(query)
        run.add_step("llm_response", input=query, output=result, tokens=80, latency=500)
        return result

enqueue() is synchronous and non-blocking (queue.put_nowait), so it is safe to call from async code without await.

Decorator

Wrap a function so every call is tracked automatically:

@agenttrace.traced_run("search-agent")          # explicit name
def search_agent(query: str) -> str:
    ...

@agenttrace.traced_run                          # uses function name
def code_agent(question: str) -> str:
    ...

The decorator uses track_run internally, so exceptions are caught, the run is marked failed, and the error is recorded before re-raising.

Marking a run failed

The context manager catches unhandled exceptions automatically. For explicit failure paths:

with agenttrace.track_run("my-agent") as run:
    result = call_external_api()
    if result is None:
        run.fail("External API returned no data")
        return
    run.add_step(...)

Configuration

Environment variables

Variable Default Description
AGENTTRACE_API_KEY Required. Your API key (at_...)
AGENTTRACE_URL http://localhost:3001 Backend base URL
AGENTTRACE_SERVICE agent Default service/agent name

agenttrace.init() — programmatic config

Calling init() is optional if you use environment variables. Use it to override defaults or tune worker behaviour:

agenttrace.init(
    api_key="at_xxxxxxxxxxxxxxxxxxxx",
    base_url="https://your-dashboard.com",
    service_name="my-agent",

    # Worker tuning (optional)
    batch_size=30,          # flush after this many runs accumulate (default: 20)
    flush_interval=3.0,     # flush every N seconds regardless (default: 2.0)
    max_queue_size=2000,    # drop runs with a warning if queue exceeds this (default: 1000)
    max_retries=5,          # retry attempts per run on transient errors (default: 4)
    retry_base_delay=1.0,   # base backoff in seconds, doubles each retry (default: 0.5)
)

init() can be called multiple times (e.g. in tests) — it replaces the worker and config.


run.add_step() reference

run.add_step(
    step_type,       # str  — see Step types below
    *,
    input="",        # str  — prompt / query / tool input
    output="",       # str  — completion / result / tool output
    tokens=0,        # int  — total tokens for this step
    latency=0,       # int  — wall-clock time in milliseconds
    cost=0.0,        # float — USD cost for this step
    status="success" # "success" | "failed"
)

Tokens and cost are summed automatically across all steps — you don't need to track totals yourself.

Step types

step_type When to use
"llm_response" Any LLM completion call
"llm_prompt" Prompt-only span (before response arrives)
"tool_call" External tool / function call
"tool_response" Response from a tool
"user_prompt" Initial user message
"decision" Routing / branching logic in the agent

Flushing before exit

The background worker sends continuously in long-running processes (servers, workers). For short-lived scripts, call flush() before the process exits:

if __name__ == "__main__":
    run_agent(query)
    agenttrace.flush()          # waits up to 10s for the queue to drain

An atexit handler provides a best-effort flush as a safety net, but an explicit flush() is more reliable.


How it works

Agent thread                     Background worker thread
─────────────────                ────────────────────────────────
track_run().__enter__()
  → AgentRun created             sleeping (flush_interval elapsed?)
run.add_step(...)
  → appended to run._steps
track_run().__exit__()
  → run.to_payload()             wakes up: batch_size reached or
  → queue.put_nowait(payload)      flush_interval elapsed
                                 drains up to batch_size items
                                 POST /agent-metrics  (with retry)
                                 POST /agent-metrics
                                 ...

The agent thread never waits on the network. If the queue fills up (e.g. backend is down for a long time), new runs are dropped with a warning rather than blocking.


Architecture

agenttrace/
  __init__.py   Public API: track_run, async_track_run, traced_run, init, flush
  _config.py    Config dataclass — written once at init, read-only thereafter
  _run.py       AgentRun — per-invocation context object, lives on the call stack
  _worker.py    IngestionWorker — daemon thread, Queue consumer, batching + atexit
  _client.py    HTTP POST with exponential backoff retry, stdlib urllib only
  py.typed      PEP 561 marker — enables type checking in downstream projects

Full example

import json, os, time
from dotenv import load_dotenv
load_dotenv()

import agenttrace
from groq import Groq

GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")

def run_agent(query: str) -> str:
    client = Groq()
    messages = [{"role": "user", "content": query}]

    with agenttrace.track_run("my-agent", model=GROQ_MODEL) as run:
        for _ in range(10):
            t0 = time.time_ns()
            resp = client.chat.completions.create(
                model=GROQ_MODEL, messages=messages, tools=[...], tool_choice="auto"
            )
            run.add_step(
                "llm_response",
                input=messages[-1]["content"],
                output=resp.choices[0].message.content or "",
                tokens=(resp.usage.prompt_tokens + resp.usage.completion_tokens),
                latency=(time.time_ns() - t0) // 1_000_000,
            )

            msg = resp.choices[0].message
            if not msg.tool_calls:
                return msg.content

            for tc in msg.tool_calls:
                args = json.loads(tc.function.arguments)
                t1 = time.time_ns()
                result = my_tool(**args)
                run.add_step(
                    "tool_call",
                    input=json.dumps(args),
                    output=str(result)[:2000],
                    latency=(time.time_ns() - t1) // 1_000_000,
                )
                messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})

        run.fail("Max iterations reached")
        return ""

if __name__ == "__main__":
    print(run_agent("best hotels in Bangalore"))
    agenttrace.flush()

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agentpulse_py-0.2.0.tar.gz (16.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agentpulse_py-0.2.0-py3-none-any.whl (22.4 kB view details)

Uploaded Python 3

File details

Details for the file agentpulse_py-0.2.0.tar.gz.

File metadata

  • Download URL: agentpulse_py-0.2.0.tar.gz
  • Upload date:
  • Size: 16.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for agentpulse_py-0.2.0.tar.gz
Algorithm Hash digest
SHA256 073c9bd781f99e8cc91fe523897ee1db7fe06a1c1fd559bccf35b6bc25e9390e
MD5 3267af0a73f87711515cab4ec05d248b
BLAKE2b-256 b0247720179af5b1653c9ba3f085530324879c7e1af5630006e66f35d48d2316

See more details on using hashes here.

File details

Details for the file agentpulse_py-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: agentpulse_py-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 22.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for agentpulse_py-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 88af78d1527c4deb94ff6348f1241b53da3ed66dc793378bd06da3fff2c9dde7
MD5 be7dcfea8503524dbe5009dcad9185c1
BLAKE2b-256 fc1e7e6a318ad50cdb53fe5f55c7db0baf8e158c8dcd71e6658f54d79bb4ea1b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page