agent-trajectory-diff 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/.gitignore +5 -0
  2. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/PKG-INFO +2 -2
  3. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/README.md +1 -1
  4. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/__init__.py +1 -1
  5. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/adapters/langfuse.py +34 -7
  6. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/adapters/openai_agents.py +5 -1
  7. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/adapters/openinference.py +12 -2
  8. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/testing/assertions.py +1 -0
  9. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/LICENSE +0 -0
  10. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/pyproject.toml +0 -0
  11. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/__main__.py +0 -0
  12. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/adapters/__init__.py +0 -0
  13. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/adapters/_iso.py +0 -0
  14. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/adapters/base.py +0 -0
  15. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/adapters/generic.py +0 -0
  16. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/adapters/langsmith.py +0 -0
  17. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/ci/baseline.py +0 -0
  18. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/ci/github.py +0 -0
  19. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/cli.py +0 -0
  20. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/config.py +0 -0
  21. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/engine/__init__.py +0 -0
  22. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/engine/aligner.py +0 -0
  23. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/engine/comparator.py +0 -0
  24. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/engine/explanations.py +0 -0
  25. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/engine/loop_detector.py +0 -0
  26. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/engine/metrics.py +0 -0
  27. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/engine/tree.py +0 -0
  28. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/loader.py +0 -0
  29. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/models/__init__.py +0 -0
  30. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/models/report.py +0 -0
  31. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/models/step.py +0 -0
  32. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/models/trace.py +0 -0
  33. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/py.typed +0 -0
  34. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/pytest_plugin.py +0 -0
  35. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/reporters/__init__.py +0 -0
  36. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/reporters/markdown.py +0 -0
  37. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/reporters/pr.py +0 -0
  38. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/reporters/terminal.py +0 -0
  39. {agent_trajectory_diff-0.2.0 → agent_trajectory_diff-0.2.2}/src/agentdiff/testing/__init__.py +0 -0
@@ -2,6 +2,11 @@
2
2
  # AGENTDIFF GITIGNORE
3
3
  # ==========================================
4
4
 
5
+ # 0. Secrets — never commit API keys
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
5
10
  # 1. Project Context (User Request)
6
11
  /context/
7
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-trajectory-diff
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A developer-first package to evaluate and find regressions in agent trajectories
5
5
  Project-URL: Homepage, https://github.com/lostmartian/agentdiff
6
6
  Project-URL: Repository, https://github.com/lostmartian/agentdiff
@@ -153,7 +153,7 @@ jobs:
153
153
  - uses: actions/setup-python@v5
154
154
  with:
155
155
  python-version: "3.11"
156
- - uses: lostmartian/agentdiff/.github/actions/agentdiff-check@v0.2.0
156
+ - uses: lostmartian/agentdiff/.github/actions/agentdiff-check@v0.2.2
157
157
  with:
158
158
  baseline: traces/baseline.json # committed baseline trace
159
159
  candidate: traces/candidate.json # generated by an earlier step
@@ -133,7 +133,7 @@ jobs:
133
133
  - uses: actions/setup-python@v5
134
134
  with:
135
135
  python-version: "3.11"
136
- - uses: lostmartian/agentdiff/.github/actions/agentdiff-check@v0.2.0
136
+ - uses: lostmartian/agentdiff/.github/actions/agentdiff-check@v0.2.2
137
137
  with:
138
138
  baseline: traces/baseline.json # committed baseline trace
139
139
  candidate: traces/candidate.json # generated by an earlier step
@@ -29,7 +29,7 @@ Typical flow::
29
29
  assert_no_regressions(report)
30
30
  """
31
31
 
32
- __version__ = "0.2.0"
32
+ __version__ = "0.2.2"
33
33
 
34
34
  from agentdiff.adapters import (
35
35
  BaseAdapter,
@@ -6,6 +6,14 @@ from agentdiff.models.step import StepStatus, StepType, TokenUsage, TraceStep
6
6
  from agentdiff.models.trace import AgentTrace
7
7
 
8
8
 
9
+ def _pick(mapping: dict[str, Any], *keys: str) -> Any:
10
+ """Return the first present value among the given key variants."""
11
+ for key in keys:
12
+ if mapping.get(key) is not None:
13
+ return mapping[key]
14
+ return None
15
+
16
+
9
17
  class LangfuseAdapter(BaseAdapter):
10
18
  @classmethod
11
19
  def from_dict(cls, data: dict[str, Any]) -> AgentTrace:
@@ -26,6 +34,21 @@ class LangfuseAdapter(BaseAdapter):
26
34
  observations = data.get("observations")
27
35
  if not isinstance(observations, list):
28
36
  observations = []
37
+
38
+ # The API may return observations in non-chronological order; sort by
39
+ # start time so step order reflects the actual execution timeline.
40
+ def _start_key(obs):
41
+ raw = _pick(obs, "startTime", "start_time")
42
+ ts = parse_iso_timestamp(raw)
43
+ if ts is not None:
44
+ return ts
45
+ return None
46
+
47
+ keyed = [(obs, _start_key(obs)) for obs in observations]
48
+ keyed = [(obs, k) for obs, k in keyed if isinstance(obs, dict)]
49
+ keyed.sort(key=lambda pair: (pair[1] is None, pair[1] or 0))
50
+ observations = [obs for obs, _ in keyed]
51
+
29
52
  steps: list[TraceStep] = []
30
53
 
31
54
  for idx, obs in enumerate(observations):
@@ -34,12 +57,16 @@ class LangfuseAdapter(BaseAdapter):
34
57
  f"Langfuse observation at index {idx} is not an object"
35
58
  )
36
59
  obs_id = obs.get("id") or f"obs_{idx}"
37
- parent_id = obs.get("parentObservationId")
60
+ parent_id = _pick(obs, "parentObservationId", "parent_observation_id")
38
61
 
39
62
  # Map Langfuse type to StepType
40
63
  obs_type = str(obs.get("type", "")).upper()
41
- if obs_type == "GENERATION":
64
+ if obs_type in ("GENERATION", "EMBEDDING"):
42
65
  step_type = StepType.LLM_CALL
66
+ elif obs_type in ("TOOL", "RETRIEVER"):
67
+ step_type = StepType.TOOL_CALL
68
+ elif obs_type in ("AGENT", "CHAIN", "GUARDRAIL", "EVALUATOR"):
69
+ step_type = StepType.ROUTING
43
70
  elif obs_type == "SPAN":
44
71
  name_lower = str(obs.get("name", "")).lower()
45
72
  if "tool" in name_lower or "call" in name_lower:
@@ -83,8 +110,8 @@ class LangfuseAdapter(BaseAdapter):
83
110
 
84
111
  # Latency
85
112
  latency_ms = 0.0
86
- start_str = obs.get("startTime")
87
- end_str = obs.get("endTime")
113
+ start_str = _pick(obs, "startTime", "start_time")
114
+ end_str = _pick(obs, "endTime", "end_time")
88
115
  if start_str and end_str:
89
116
  t1 = parse_iso_timestamp(start_str)
90
117
  t2 = parse_iso_timestamp(end_str)
@@ -92,7 +119,7 @@ class LangfuseAdapter(BaseAdapter):
92
119
  latency_ms = max(0.0, (t2 - t1).total_seconds() * 1000.0)
93
120
  if latency_ms == 0.0:
94
121
  latency_ms = (
95
- cls._as_float(obs.get("latency_ms"))
122
+ cls._as_float(_pick(obs, "latency_ms", "latency"))
96
123
  or cls._as_float(obs.get("duration") or 0.0) * 1000.0
97
124
  )
98
125
 
@@ -100,9 +127,9 @@ class LangfuseAdapter(BaseAdapter):
100
127
  level = str(obs.get("level", "")).upper()
101
128
  status = StepStatus.SUCCESS
102
129
  error_message = None
103
- if "ERROR" in level or obs.get("statusMessage"):
130
+ if "ERROR" in level or _pick(obs, "statusMessage", "status_message"):
104
131
  status = StepStatus.ERROR
105
- error_message = obs.get("statusMessage")
132
+ error_message = _pick(obs, "statusMessage", "status_message")
106
133
 
107
134
  step = TraceStep(
108
135
  step_id=obs_id,
@@ -127,7 +127,11 @@ class OpenAIAgentsAdapter(BaseAdapter):
127
127
  if not isinstance(span_data, dict):
128
128
  span_data = {}
129
129
  stype = str(span_data.get("type") or span.get("type") or "").lower()
130
- if stype in _SKIPPED_TYPES:
130
+ sname = str(span_data.get("name") or "").lower()
131
+ # Real OpenAI Agents traces mark the task/turn bookkeeping spans as
132
+ # type="custom" with name="task"/"turn"; older/fixture shapes used a
133
+ # dedicated type. Skip both so they don't surface as steps.
134
+ if stype in _SKIPPED_TYPES or (stype == "custom" and sname in _SKIPPED_TYPES):
131
135
  return None
132
136
 
133
137
  step_type = _TYPE_MAP.get(stype, StepType.THOUGHT)
@@ -31,8 +31,12 @@ class OpenInferenceAdapter(BaseAdapter):
31
31
  context = span.get("context")
32
32
  if not isinstance(context, dict):
33
33
  context = {}
34
- span_id = context.get("span_id") or span.get("span_id") or f"span_{idx}"
34
+ span_id = str(
35
+ context.get("span_id") or span.get("span_id") or f"span_{idx}"
36
+ )
35
37
  parent_id = span.get("parent_span_id") or span.get("parent_id")
38
+ if parent_id is not None:
39
+ parent_id = str(parent_id)
36
40
 
37
41
  attrs = span.get("attributes")
38
42
  if not isinstance(attrs, dict):
@@ -100,7 +104,12 @@ class OpenInferenceAdapter(BaseAdapter):
100
104
  if isinstance(start_time, (int, float)) and isinstance(
101
105
  end_time, (int, float)
102
106
  ):
103
- latency_ms = (end_time - start_time) * 1000.0
107
+ # OTel spans carry nanoseconds (~1e18); the legacy shape
108
+ # uses seconds. Detect ns by magnitude and normalize.
109
+ scale = 1.0
110
+ if abs(end_time) > 1e11:
111
+ scale = 1e-9
112
+ latency_ms = max(0.0, (end_time - start_time) * scale * 1000.0)
104
113
  else:
105
114
  t1 = parse_iso_timestamp(start_time)
106
115
  t2 = parse_iso_timestamp(end_time)
@@ -155,6 +164,7 @@ class OpenInferenceAdapter(BaseAdapter):
155
164
  or first_span.get("trace_id")
156
165
  or trace_id
157
166
  )
167
+ trace_id = str(trace_id)
158
168
 
159
169
  # Task input / Final output from root span if available
160
170
  task_input = {}
@@ -51,6 +51,7 @@ def assert_no_regressions(
51
51
  )
52
52
 
53
53
  if errors:
54
+ report.passed = False
54
55
  errors_str = "\n".join(f" - {err}" for err in errors)
55
56
  summary_str = report.summary()
56
57
  raise AssertionError(