Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 36 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,66 @@ on:
pull_request:
branches: [main]

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[all]"
pip install -e "."
pip install pytest pytest-cov
- name: Run tests
run: |
pytest tests/ -v --cov=agent_trace --cov-report=xml
- name: Upload coverage
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v4
if: matrix.python-version == '3.14'
uses: codecov/codecov-action@v7
with:
files: ./coverage.xml
fail_ci_if_error: false

optional-dependencies:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
file: ./coverage.xml
python-version: "3.13"
cache: pip
- name: Install all optional dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[all]"
pip install pytest pytest-cov
- name: Run tests with all optional dependencies
run: pytest tests/ -q

lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
python-version: "3.14"
cache: pip
- name: Install dependencies
run: pip install ruff
- name: Lint
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ traceweave/
│ ├── exporters/ # Export to JSON, Chrome Trace, etc.
│ └── cli.py # CLI: traceweave tui|dashboard|demo|export
├── examples/ # Demo scripts (no API keys needed)
└── tests/ # 39 tests, 100% passing
└── tests/ # Automated test suite
```

| Technology | Purpose |
Expand Down Expand Up @@ -378,7 +378,7 @@ MIT License — see [LICENSE](LICENSE) for details.
[license-shield]: https://img.shields.io/badge/license-MIT-369eff?labelColor=black&style=flat-square
[license-link]: https://opensource.org/licenses/MIT
[downloads-shield]: https://img.shields.io/pypi/dm/traceweave?color=369eff&labelColor=black&style=flat-square
[test-shield]: https://img.shields.io/badge/tests-39%20passed-369eff?labelColor=black&logo=pytest&logoColor=white&style=flat-square
[test-shield]: https://github.com/weivwang/trace-wave/actions/workflows/ci.yml/badge.svg
[test-link]: https://github.com/weivwang/trace-wave/actions
[docs-link]: https://github.com/weivwang/trace-wave#-quick-start
[issues-link]: https://github.com/weivwang/trace-wave/issues
10 changes: 5 additions & 5 deletions agent_trace/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
"""traceweave: Distributed tracing and observability for AI agents."""

__version__ = "0.1.0"
__version__ = "0.1.2"

from agent_trace.core.context import get_current_span, get_current_trace
from agent_trace.core.decorators import trace_agent, trace_llm, trace_tool
from agent_trace.core.models import (
SpanData,
SpanEvent,
SpanKind,
SpanStatus,
TokenUsage,
SpanEvent,
SpanData,
TraceData,
)
from agent_trace.core.span import Span
from agent_trace.core.context import get_current_span, get_current_trace
from agent_trace.core.tracer import AgentTracer, tracer
from agent_trace.core.decorators import trace_agent, trace_tool, trace_llm

__all__ = [
"__version__",
Expand Down
7 changes: 4 additions & 3 deletions agent_trace/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
traceweave export trace.json --format chrome -o trace.chrome.json
traceweave demo
"""

import click
import json
import sys

from agent_trace import __version__


@click.group()
@click.version_option(version="0.1.0", prog_name="traceweave")
@click.version_option(version=__version__, prog_name="traceweave")
def main():
"""🔍 traceweave: Distributed tracing and observability for AI agents."""
pass
Expand Down
16 changes: 8 additions & 8 deletions agent_trace/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
"""Core tracing components for traceweave."""

from agent_trace.core.context import (
get_current_span,
get_current_trace,
set_current_span,
set_current_trace,
)
from agent_trace.core.models import (
SpanData,
SpanEvent,
SpanKind,
SpanStatus,
TokenUsage,
SpanEvent,
SpanData,
TraceData,
)
from agent_trace.core.span import Span
from agent_trace.core.context import (
get_current_span,
set_current_span,
get_current_trace,
set_current_trace,
)
from agent_trace.core.tracer import AgentTracer, tracer

__all__ = [
Expand Down
2 changes: 2 additions & 0 deletions agent_trace/core/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# ... nested code sees my_span via get_current_span() ...
reset_current_span(token)
"""

from __future__ import annotations

import contextvars
Expand All @@ -32,6 +33,7 @@

# ── Public helpers ───────────────────────────────────────────────────────


def get_current_span() -> Optional[Span]:
"""Return the currently active span, or ``None`` if outside a trace."""
return _current_span.get()
Expand Down
11 changes: 4 additions & 7 deletions agent_trace/core/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ def search(query: str) -> list[str]:
def generate(prompt: str) -> str:
return call_llm(prompt)
"""

import functools
import inspect
from contextlib import contextmanager
from typing import Any, Callable, Optional, TypeVar

from agent_trace.core.models import SpanKind
from agent_trace.core.context import get_current_trace
from agent_trace.core.models import SpanKind
from agent_trace.core.tracer import tracer as default_tracer

F = TypeVar("F", bound=Callable[..., Any])
Expand Down Expand Up @@ -296,9 +297,7 @@ def _serialize_output(value: Any) -> Any:
if hasattr(value, "model_dump"):
return value.model_dump()
if hasattr(value, "__dict__"):
return {
k: str(v) for k, v in value.__dict__.items() if not k.startswith("_")
}
return {k: str(v) for k, v in value.__dict__.items() if not k.startswith("_")}
except Exception:
pass
return str(value)[:1000]
Expand Down Expand Up @@ -327,6 +326,4 @@ def _try_extract_usage(span: Any, result: Any) -> None:
usage.get("completion_tokens", 0) if isinstance(usage, dict) else 0
)
if prompt_tokens or completion_tokens:
span.set_token_usage(
prompt_tokens=prompt_tokens, completion_tokens=completion_tokens
)
span.set_token_usage(prompt_tokens=prompt_tokens, completion_tokens=completion_tokens)
3 changes: 2 additions & 1 deletion agent_trace/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
tracing system: spans, traces, token usage, and events. These models
form the data layer that all other components build upon.
"""

from __future__ import annotations

import uuid
Expand Down Expand Up @@ -174,4 +175,4 @@ def _count_spans(self, span: SpanData) -> int:
model_config = {
"arbitrary_types_allowed": True,
"protected_namespaces": (),
}
}
3 changes: 2 additions & 1 deletion agent_trace/core/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
The span automatically records timing, captures exceptions, and notifies
the parent :class:`~agent_trace.core.tracer.AgentTracer` on completion.
"""

from __future__ import annotations

import traceback
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Optional

from agent_trace.core.models import SpanData, SpanKind, SpanStatus, SpanEvent, TokenUsage
from agent_trace.core.models import SpanData, SpanEvent, SpanKind, SpanStatus, TokenUsage

if TYPE_CHECKING:
from agent_trace.core.tracer import AgentTracer
Expand Down
52 changes: 42 additions & 10 deletions agent_trace/core/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,25 @@
result = plan()
span.set_output(result)
"""

from __future__ import annotations

import threading
import uuid
from contextlib import contextmanager
from datetime import datetime, timezone
from typing import Any, Callable, Optional
from contextlib import contextmanager

from agent_trace.core.models import SpanData, SpanKind, SpanStatus, TraceData
from agent_trace.core.span import Span
from agent_trace.core.context import (
get_current_span,
set_current_span,
reset_current_span,
get_current_trace,
set_current_trace,
reset_current_span,
reset_current_trace,
set_current_span,
set_current_trace,
)
from agent_trace.core.models import SpanData, SpanKind, TraceData
from agent_trace.core.span import Span

# Type alias for event listener callbacks
EventListener = Callable[[str, dict[str, Any]], None]
Expand Down Expand Up @@ -177,7 +178,8 @@ def start_span(
The new :class:`Span`.
"""
parent_span = get_current_span()
trace_id = get_current_trace() or uuid.uuid4().hex
current_trace_id = get_current_trace()
trace_id = current_trace_id or uuid.uuid4().hex

span_data = SpanData(
trace_id=trace_id,
Expand All @@ -193,6 +195,24 @@ def start_span(
if parent_span:
parent_span._data.children.append(span_data)

# Auto-instrumentations call start_span() directly. When there is no
# surrounding trace, retain that operation as a one-span trace instead
# of silently discarding it when the span ends.
standalone_trace = None
trace_token = None
if parent_span is None and current_trace_id is None:
standalone_trace = TraceData(
trace_id=trace_id,
name=name,
start_time=span_data.start_time,
root_span=span_data,
metadata={},
)
with self._lock:
self._traces[trace_id] = standalone_trace
trace_token = set_current_trace(trace_id)
self._emit("trace_start", {"trace_id": trace_id, "name": name})

with self._lock:
self._active_spans[span_data.span_id] = span

Expand All @@ -218,6 +238,20 @@ def start_span(
span.end()
# Restore previous span context using proper reset
reset_current_span(old_span_token)
if standalone_trace is not None:
standalone_trace.end_time = span_data.end_time
if trace_token is not None:
reset_current_trace(trace_token)
self._emit(
"trace_end",
{
"trace_id": trace_id,
"duration_ms": standalone_trace.total_duration_ms,
"total_tokens": standalone_trace.total_tokens,
"total_cost": standalone_trace.total_cost,
"span_count": standalone_trace.span_count,
},
)

# ── Internal callbacks ───────────────────────────────────────────

Expand All @@ -235,9 +269,7 @@ def _on_span_end(self, span: Span) -> None:
"status": span._data.status.value,
"duration_ms": span._data.duration_ms,
"token_usage": (
span._data.token_usage.model_dump()
if span._data.token_usage
else None
span._data.token_usage.model_dump() if span._data.token_usage else None
),
"error": span._data.error,
},
Expand Down
3 changes: 2 additions & 1 deletion agent_trace/dashboard/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Dashboard components for traceweave."""

from agent_trace.dashboard.server import run_server
from agent_trace.dashboard.tui import TraceDashboard, print_trace, run_tui
from agent_trace.dashboard.trace_viewer import view_trace_file
from agent_trace.dashboard.tui import TraceDashboard, print_trace, run_tui

__all__ = [
"run_server",
Expand Down
Loading