steplight 0.1.0__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. steplight-0.1.0/LICENSE +21 -0
  2. steplight-0.1.0/PKG-INFO +181 -0
  3. steplight-0.1.0/README.md +145 -0
  4. steplight-0.1.0/pyproject.toml +85 -0
  5. steplight-0.1.0/setup.cfg +4 -0
  6. steplight-0.1.0/steplight/__init__.py +5 -0
  7. steplight-0.1.0/steplight/adapters/__init__.py +1 -0
  8. steplight-0.1.0/steplight/adapters/common.py +30 -0
  9. steplight-0.1.0/steplight/adapters/generic.py +156 -0
  10. steplight-0.1.0/steplight/adapters/langchain.py +84 -0
  11. steplight-0.1.0/steplight/adapters/mcp.py +58 -0
  12. steplight-0.1.0/steplight/adapters/openai.py +102 -0
  13. steplight-0.1.0/steplight/cli/__init__.py +1 -0
  14. steplight-0.1.0/steplight/cli/config.py +30 -0
  15. steplight-0.1.0/steplight/cli/main.py +166 -0
  16. steplight-0.1.0/steplight/core/__init__.py +1 -0
  17. steplight-0.1.0/steplight/core/analyzer.py +229 -0
  18. steplight-0.1.0/steplight/core/models.py +70 -0
  19. steplight-0.1.0/steplight/core/parser.py +79 -0
  20. steplight-0.1.0/steplight/core/stats.py +98 -0
  21. steplight-0.1.0/steplight/export/__init__.py +1 -0
  22. steplight-0.1.0/steplight/export/html.py +24 -0
  23. steplight-0.1.0/steplight/export/templates/report.html.jinja +617 -0
  24. steplight-0.1.0/steplight/tui/__init__.py +1 -0
  25. steplight-0.1.0/steplight/tui/app.py +95 -0
  26. steplight-0.1.0/steplight/tui/detail_panel.py +28 -0
  27. steplight-0.1.0/steplight/tui/diagnostics.py +17 -0
  28. steplight-0.1.0/steplight/tui/timeline.py +25 -0
  29. steplight-0.1.0/steplight.egg-info/PKG-INFO +181 -0
  30. steplight-0.1.0/steplight.egg-info/SOURCES.txt +37 -0
  31. steplight-0.1.0/steplight.egg-info/dependency_links.txt +1 -0
  32. steplight-0.1.0/steplight.egg-info/entry_points.txt +3 -0
  33. steplight-0.1.0/steplight.egg-info/requires.txt +14 -0
  34. steplight-0.1.0/steplight.egg-info/top_level.txt +1 -0
  35. steplight-0.1.0/tests/test_analyzer.py +24 -0
  36. steplight-0.1.0/tests/test_cli.py +51 -0
  37. steplight-0.1.0/tests/test_export_html.py +63 -0
  38. steplight-0.1.0/tests/test_parser.py +32 -0
  39. steplight-0.1.0/tests/test_tui.py +19 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Riccardo Merenda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: steplight
3
+ Version: 0.1.0
4
+ Summary: Local-first trace inspector for LLM agents and tool-driven workflows.
5
+ Author: Riccardo Merenda
6
+ License-Expression: MIT
7
+ Keywords: llm,agents,tracing,observability,tui,cli,debugging
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Software Development :: Testing
18
+ Classifier: Topic :: Terminals
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: jinja2>=3.1
23
+ Requires-Dist: pygments>=2.17
24
+ Requires-Dist: pydantic>=2.7
25
+ Requires-Dist: pyyaml>=6.0
26
+ Requires-Dist: rich>=13.7
27
+ Requires-Dist: textual>=0.76
28
+ Requires-Dist: typer>=0.12
29
+ Provides-Extra: dev
30
+ Requires-Dist: build>=1.2; extra == "dev"
31
+ Requires-Dist: pytest>=8.2; extra == "dev"
32
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
33
+ Requires-Dist: ruff>=0.6; extra == "dev"
34
+ Requires-Dist: twine>=5.1; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # Steplight
38
+
39
+ Local-first trace inspector for LLM agents and tool-driven workflows.
40
+
41
+ > Load a trace. See what happened. Understand why.
42
+
43
+ Steplight is a terminal-native tool for inspecting LLM runs, tool calls, retries, token usage, errors, and execution bottlenecks without sending trace data to a third-party platform.
44
+
45
+ It is designed for developers building with OpenAI-style workflows, LangChain, MCP tooling, or custom JSON/YAML traces who want a fast local debugging loop and a shareable HTML report.
46
+
47
+ ## Why Steplight
48
+
49
+ - Understand what your agent actually did, step by step
50
+ - Spot slow tools, retry loops, and cost spikes quickly
51
+ - Keep sensitive traces on your own machine
52
+ - Stay in the terminal instead of digging through raw JSON by hand
53
+ - Share a polished HTML report when you need feedback from teammates
54
+
55
+ ## Features
56
+
57
+ - Interactive Textual inspector for timelines, details, and diagnostics
58
+ - Rich terminal summary for quick debugging and CI-friendly output
59
+ - Static HTML export for sharing a run with other people
60
+ - Built-in diagnostics for common agent failure patterns
61
+ - Support for OpenAI-style traces, LangChain callbacks, MCP logs, and generic JSON/YAML
62
+ - Extensible adapters and rules so you can grow it with your workflow
63
+
64
+ ## Install
65
+
66
+ ```bash
67
+ pip install steplight
68
+ ```
69
+
70
+ The package installs both `steplight` and the shorter `slt` alias. The examples below use `slt` to avoid PowerShell's reserved `sl` alias.
71
+
72
+ ## Quick Start
73
+
74
+ ```bash
75
+ slt summary sample_traces/agent_with_tools.json
76
+ slt validate sample_traces/simple_qa.json
77
+ slt export sample_traces/expensive_run.json -o report.html
78
+ slt inspect sample_traces/agent_with_tools.json
79
+ ```
80
+
81
+ ## Example Summary Output
82
+
83
+ ```text
84
+ +------------------------ Steplight Summary ------------------------+
85
+ | Run: Find compliance gaps in policy |
86
+ | Source: openai |
87
+ | Overview: Duration: 14.0s | Steps: 4 | Tool calls: 2 | Retries: 0 |
88
+ | Tokens: 4,230 in / 670 out | Est. cost: $0.0028 |
89
+ +-------------------------------------------------------------------+
90
+
91
+ Diagnostics
92
+ - WARNING: Step 'web_search' took 68.6% of total runtime.
93
+ - WARNING: Tool 'web_search' took 9.6s. Check timeouts, caching, or external latency.
94
+ - INFO: Input tokens grew 4.2x between 'Draft plan' and 'Final answer'.
95
+ Bottleneck: web_search (68.6% of total runtime)
96
+ ```
97
+
98
+ ## Supported Trace Formats
99
+
100
+ - OpenAI-style step traces
101
+ - LangChain callback event exports
102
+ - MCP tool call logs
103
+ - Generic JSON or YAML with an optional `steplight.yaml` mapping file
104
+
105
+ ## Diagnostics
106
+
107
+ Steplight currently flags:
108
+
109
+ - bottlenecks
110
+ - retry loops
111
+ - context growth
112
+ - repeated tool calls
113
+ - silent errors
114
+ - slow tools
115
+ - high-cost runs
116
+ - empty completions
117
+
118
+ ## Custom Mapping Example
119
+
120
+ ```yaml
121
+ mapping:
122
+ steps_path: "$.events"
123
+ timestamp_field: "ts"
124
+ type_field: "event_type"
125
+ type_values:
126
+ prompt: "llm_start"
127
+ completion: "llm_end"
128
+ tool_call: "tool_start"
129
+ tool_result: "tool_end"
130
+ ```
131
+
132
+ ## Commands
133
+
134
+ - `slt inspect <file>` opens the interactive Textual UI
135
+ - `slt summary <file>` prints a non-interactive terminal summary
136
+ - `slt export <file> -o report.html` creates a static HTML report
137
+ - `slt validate <file>` checks whether a trace can be parsed successfully
138
+
139
+ ## Docs
140
+
141
+ - [Roadmap](docs/ROADMAP.md)
142
+ - [Versioning and Releases](docs/VERSIONING.md)
143
+ - [Release Process](RELEASING.md)
144
+
145
+ ## Repository Layout
146
+
147
+ ```text
148
+ steplight/
149
+ cli/
150
+ core/
151
+ adapters/
152
+ tui/
153
+ export/
154
+ sample_traces/
155
+ tests/
156
+ ```
157
+
158
+ ## Development
159
+
160
+ ```bash
161
+ python -m pip install -e .[dev]
162
+ pytest
163
+ python -m steplight.cli.main --help
164
+ ```
165
+
166
+ ## Release Checklist
167
+
168
+ - Run `pytest`
169
+ - Build the distribution with `python -m build`
170
+ - Update `CHANGELOG.md`
171
+ - Review the generated wheel and sdist
172
+ - Publish the repository to GitHub
173
+ - Publish the package to PyPI when ready
174
+
175
+ ## Status
176
+
177
+ Steplight is currently an alpha-stage project focused on a clean local-first inspection experience, a solid parser surface, and practical diagnostics.
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,145 @@
1
+ # Steplight
2
+
3
+ Local-first trace inspector for LLM agents and tool-driven workflows.
4
+
5
+ > Load a trace. See what happened. Understand why.
6
+
7
+ Steplight is a terminal-native tool for inspecting LLM runs, tool calls, retries, token usage, errors, and execution bottlenecks without sending trace data to a third-party platform.
8
+
9
+ It is designed for developers building with OpenAI-style workflows, LangChain, MCP tooling, or custom JSON/YAML traces who want a fast local debugging loop and a shareable HTML report.
10
+
11
+ ## Why Steplight
12
+
13
+ - Understand what your agent actually did, step by step
14
+ - Spot slow tools, retry loops, and cost spikes quickly
15
+ - Keep sensitive traces on your own machine
16
+ - Stay in the terminal instead of digging through raw JSON by hand
17
+ - Share a polished HTML report when you need feedback from teammates
18
+
19
+ ## Features
20
+
21
+ - Interactive Textual inspector for timelines, details, and diagnostics
22
+ - Rich terminal summary for quick debugging and CI-friendly output
23
+ - Static HTML export for sharing a run with other people
24
+ - Built-in diagnostics for common agent failure patterns
25
+ - Support for OpenAI-style traces, LangChain callbacks, MCP logs, and generic JSON/YAML
26
+ - Extensible adapters and rules so you can grow it with your workflow
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install steplight
32
+ ```
33
+
34
+ The package installs both `steplight` and the shorter `slt` alias. The examples below use `slt` to avoid PowerShell's reserved `sl` alias.
35
+
36
+ ## Quick Start
37
+
38
+ ```bash
39
+ slt summary sample_traces/agent_with_tools.json
40
+ slt validate sample_traces/simple_qa.json
41
+ slt export sample_traces/expensive_run.json -o report.html
42
+ slt inspect sample_traces/agent_with_tools.json
43
+ ```
44
+
45
+ ## Example Summary Output
46
+
47
+ ```text
48
+ +------------------------ Steplight Summary ------------------------+
49
+ | Run: Find compliance gaps in policy |
50
+ | Source: openai |
51
+ | Overview: Duration: 14.0s | Steps: 4 | Tool calls: 2 | Retries: 0 |
52
+ | Tokens: 4,230 in / 670 out | Est. cost: $0.0028 |
53
+ +-------------------------------------------------------------------+
54
+
55
+ Diagnostics
56
+ - WARNING: Step 'web_search' took 68.6% of total runtime.
57
+ - WARNING: Tool 'web_search' took 9.6s. Check timeouts, caching, or external latency.
58
+ - INFO: Input tokens grew 4.2x between 'Draft plan' and 'Final answer'.
59
+ Bottleneck: web_search (68.6% of total runtime)
60
+ ```
61
+
62
+ ## Supported Trace Formats
63
+
64
+ - OpenAI-style step traces
65
+ - LangChain callback event exports
66
+ - MCP tool call logs
67
+ - Generic JSON or YAML with an optional `steplight.yaml` mapping file
68
+
69
+ ## Diagnostics
70
+
71
+ Steplight currently flags:
72
+
73
+ - bottlenecks
74
+ - retry loops
75
+ - context growth
76
+ - repeated tool calls
77
+ - silent errors
78
+ - slow tools
79
+ - high-cost runs
80
+ - empty completions
81
+
82
+ ## Custom Mapping Example
83
+
84
+ ```yaml
85
+ mapping:
86
+ steps_path: "$.events"
87
+ timestamp_field: "ts"
88
+ type_field: "event_type"
89
+ type_values:
90
+ prompt: "llm_start"
91
+ completion: "llm_end"
92
+ tool_call: "tool_start"
93
+ tool_result: "tool_end"
94
+ ```
95
+
96
+ ## Commands
97
+
98
+ - `slt inspect <file>` opens the interactive Textual UI
99
+ - `slt summary <file>` prints a non-interactive terminal summary
100
+ - `slt export <file> -o report.html` creates a static HTML report
101
+ - `slt validate <file>` checks whether a trace can be parsed successfully
102
+
103
+ ## Docs
104
+
105
+ - [Roadmap](docs/ROADMAP.md)
106
+ - [Versioning and Releases](docs/VERSIONING.md)
107
+ - [Release Process](RELEASING.md)
108
+
109
+ ## Repository Layout
110
+
111
+ ```text
112
+ steplight/
113
+ cli/
114
+ core/
115
+ adapters/
116
+ tui/
117
+ export/
118
+ sample_traces/
119
+ tests/
120
+ ```
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ python -m pip install -e .[dev]
126
+ pytest
127
+ python -m steplight.cli.main --help
128
+ ```
129
+
130
+ ## Release Checklist
131
+
132
+ - Run `pytest`
133
+ - Build the distribution with `python -m build`
134
+ - Update `CHANGELOG.md`
135
+ - Review the generated wheel and sdist
136
+ - Publish the repository to GitHub
137
+ - Publish the package to PyPI when ready
138
+
139
+ ## Status
140
+
141
+ Steplight is currently an alpha-stage project focused on a clean local-first inspection experience, a solid parser surface, and practical diagnostics.
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,85 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "steplight"
7
+ version = "0.1.0"
8
+ description = "Local-first trace inspector for LLM agents and tool-driven workflows."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "Riccardo Merenda" }
15
+ ]
16
+ keywords = [
17
+ "llm",
18
+ "agents",
19
+ "tracing",
20
+ "observability",
21
+ "tui",
22
+ "cli",
23
+ "debugging",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 3 - Alpha",
27
+ "Environment :: Console",
28
+ "Intended Audience :: Developers",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Topic :: Software Development :: Libraries :: Python Modules",
35
+ "Topic :: Software Development :: Testing",
36
+ "Topic :: Terminals",
37
+ ]
38
+ dependencies = [
39
+ "jinja2>=3.1",
40
+ "pygments>=2.17",
41
+ "pydantic>=2.7",
42
+ "pyyaml>=6.0",
43
+ "rich>=13.7",
44
+ "textual>=0.76",
45
+ "typer>=0.12"
46
+ ]
47
+
48
+ [project.optional-dependencies]
49
+ dev = [
50
+ "build>=1.2",
51
+ "pytest>=8.2",
52
+ "pytest-asyncio>=0.23",
53
+ "ruff>=0.6",
54
+ "twine>=5.1"
55
+ ]
56
+
57
+ [project.scripts]
58
+ steplight = "steplight.cli.main:app"
59
+ slt = "steplight.cli.main:app"
60
+
61
+ [tool.pytest.ini_options]
62
+ testpaths = ["tests"]
63
+ addopts = "-ra -p no:cacheprovider"
64
+
65
+ [tool.setuptools]
66
+ include-package-data = true
67
+
68
+ [tool.setuptools.packages.find]
69
+ include = ["steplight*"]
70
+
71
+ [tool.setuptools.package-data]
72
+ steplight = ["export/templates/*.jinja"]
73
+
74
+ [tool.ruff]
75
+ line-length = 100
76
+ target-version = "py311"
77
+
78
+ [tool.ruff.lint]
79
+ select = [
80
+ "E",
81
+ "F",
82
+ "I",
83
+ "B",
84
+ "UP",
85
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Steplight package."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """Trace format adapters."""
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Any
5
+
6
+
7
+ def parse_dt(value: Any) -> datetime:
8
+ if isinstance(value, datetime):
9
+ return value
10
+ if value is None:
11
+ return datetime.now(timezone.utc)
12
+ if isinstance(value, (int, float)):
13
+ return datetime.fromtimestamp(value, tz=timezone.utc)
14
+
15
+ normalized = str(value).replace("Z", "+00:00")
16
+ return datetime.fromisoformat(normalized)
17
+
18
+
19
+ def compact_text(value: Any, *, limit: int = 240, keep_empty: bool = False) -> str | None:
20
+ if value is None:
21
+ return None
22
+ if isinstance(value, str):
23
+ text = value.strip()
24
+ else:
25
+ text = str(value).strip()
26
+ if not text:
27
+ return "" if keep_empty else None
28
+ if len(text) <= limit:
29
+ return text
30
+ return text[: limit - 3] + "..."
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+ from steplight.adapters.common import compact_text, parse_dt
9
+ from steplight.core.models import Step, StepType, Trace
10
+
11
+
12
+ DEFAULT_MAPPING = {
13
+ "steps_path": "$.steps",
14
+ "timestamp_field": "timestamp",
15
+ "type_field": "type",
16
+ "name_field": "name",
17
+ "duration_field": "duration_ms",
18
+ "input_field": "input",
19
+ "output_field": "output",
20
+ "model_field": "model",
21
+ "tokens_in_field": "tokens_in",
22
+ "tokens_out_field": "tokens_out",
23
+ "error_field": "error",
24
+ "metadata_field": "metadata",
25
+ "type_values": {},
26
+ }
27
+
28
+
29
+ def load_mapping(config_path: Path | None) -> dict[str, Any]:
30
+ mapping = dict(DEFAULT_MAPPING)
31
+ if not config_path or not config_path.exists():
32
+ return mapping
33
+
34
+ payload = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
35
+ mapping.update(payload.get("mapping") or {})
36
+ return mapping
37
+
38
+
39
+ def parse_generic_trace(payload: dict[str, Any], mapping: dict[str, Any] | None = None) -> Trace:
40
+ active_mapping = dict(DEFAULT_MAPPING)
41
+ if mapping:
42
+ active_mapping.update(mapping)
43
+
44
+ raw_steps = _extract_path(payload, active_mapping["steps_path"])
45
+ if not isinstance(raw_steps, list):
46
+ raise ValueError("Generic mapping did not resolve to a list of steps.")
47
+
48
+ steps: list[Step] = []
49
+ type_values = active_mapping.get("type_values") or {}
50
+
51
+ for index, raw_step in enumerate(raw_steps):
52
+ if not isinstance(raw_step, dict):
53
+ raise ValueError(f"Generic step at index {index} is not an object.")
54
+ raw_type = _get_field(raw_step, active_mapping["type_field"])
55
+ normalized_type = _normalize_type(raw_type, type_values)
56
+ metadata = _get_field(raw_step, active_mapping["metadata_field"]) or {}
57
+ steps.append(
58
+ Step(
59
+ id=str(raw_step.get("id") or f"{payload.get('id', 'generic')}:step:{index}"),
60
+ type=normalized_type,
61
+ name=_get_field(raw_step, active_mapping["name_field"]),
62
+ timestamp=parse_dt(_get_field(raw_step, active_mapping["timestamp_field"])),
63
+ duration_ms=_maybe_float(_get_field(raw_step, active_mapping["duration_field"])),
64
+ input=compact_text(_get_field(raw_step, active_mapping["input_field"])),
65
+ output=compact_text(_get_field(raw_step, active_mapping["output_field"]), keep_empty=True),
66
+ model=_get_field(raw_step, active_mapping["model_field"]),
67
+ tokens_in=_maybe_int(_get_field(raw_step, active_mapping["tokens_in_field"])),
68
+ tokens_out=_maybe_int(_get_field(raw_step, active_mapping["tokens_out_field"])),
69
+ error=compact_text(_get_field(raw_step, active_mapping["error_field"])),
70
+ metadata=metadata if isinstance(metadata, dict) else {"value": metadata},
71
+ )
72
+ )
73
+
74
+ started_at = parse_dt(payload.get("started_at") or (steps[0].timestamp if steps else None))
75
+ ended_at = parse_dt(payload.get("ended_at")) if payload.get("ended_at") else _infer_ended_at(steps)
76
+
77
+ return Trace(
78
+ id=str(payload.get("id") or "generic-trace"),
79
+ name=payload.get("name"),
80
+ started_at=started_at,
81
+ ended_at=ended_at,
82
+ steps=steps,
83
+ total_tokens=_maybe_int(payload.get("total_tokens")),
84
+ total_cost_usd=_maybe_float(payload.get("total_cost_usd") or payload.get("cost_usd")),
85
+ source="generic",
86
+ status=payload.get("status"),
87
+ metadata={"raw": payload, "mapping": active_mapping},
88
+ )
89
+
90
+
91
+ def _extract_path(payload: dict[str, Any], json_path: str) -> Any:
92
+ current: Any = payload
93
+ path = json_path.strip()
94
+ if path in {"$", ""}:
95
+ return current
96
+ if path.startswith("$."):
97
+ path = path[2:]
98
+ for part in path.split("."):
99
+ if not part:
100
+ continue
101
+ if "[" in part and part.endswith("]"):
102
+ key, index_text = part[:-1].split("[", maxsplit=1)
103
+ if key:
104
+ current = current[key]
105
+ current = current[int(index_text)]
106
+ else:
107
+ current = current[part]
108
+ return current
109
+
110
+
111
+ def _get_field(item: dict[str, Any], field_name: str | None) -> Any:
112
+ if not field_name:
113
+ return None
114
+ current: Any = item
115
+ for part in str(field_name).split("."):
116
+ if not isinstance(current, dict):
117
+ return None
118
+ current = current.get(part)
119
+ return current
120
+
121
+
122
+ def _normalize_type(raw_type: Any, type_values: dict[str, str]) -> StepType:
123
+ value = str(raw_type or "").strip().lower()
124
+ for normalized, external in type_values.items():
125
+ if value == str(external).strip().lower():
126
+ return StepType(normalized)
127
+ if value in {member.value for member in StepType}:
128
+ return StepType(value)
129
+ if "tool" in value and "result" in value:
130
+ return StepType.TOOL_RESULT
131
+ if "tool" in value:
132
+ return StepType.TOOL_CALL
133
+ if "retry" in value:
134
+ return StepType.RETRY
135
+ if "error" in value:
136
+ return StepType.ERROR
137
+ if "start" in value:
138
+ return StepType.CHAIN_START
139
+ if "end" in value:
140
+ return StepType.CHAIN_END
141
+ return StepType.COMPLETION
142
+
143
+
144
+ def _maybe_int(value: Any) -> int | None:
145
+ return int(value) if value is not None else None
146
+
147
+
148
+ def _maybe_float(value: Any) -> float | None:
149
+ return float(value) if value is not None else None
150
+
151
+
152
+ def _infer_ended_at(steps: list[Step]):
153
+ if not steps:
154
+ return None
155
+ latest = max(steps, key=lambda item: item.timestamp)
156
+ return latest.timestamp