steplot 0.1.0__py3-none-any.whl

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.
steplot/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """steplot — lightweight agent step tracker with decorator and logging support."""
2
+
3
+ from .display import display_run
4
+ from .storage import load_run, save_run
5
+ from .tracker import get_current_run, get_current_step, log_event, reset, run_context, step_context, track
6
+
7
+ __all__ = [
8
+ "track",
9
+ "run_context",
10
+ "step_context",
11
+ "log_event",
12
+ "get_current_run",
13
+ "get_current_step",
14
+ "reset",
15
+ "display_run",
16
+ "save_run",
17
+ "load_run",
18
+ ]
19
+
20
+ __version__ = "0.1.0"
steplot/display.py ADDED
@@ -0,0 +1,71 @@
1
+ """Terminal display for steplot runs — renders the run tree to stdout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .models import Event, Run, Step, StepStatus
6
+
7
+
8
+ def _status_annotation(step: Step) -> str:
9
+ """Return a short status suffix for a step when it isn't a clean success."""
10
+ if step.status == StepStatus.FAILED:
11
+ return " ✗"
12
+ if step.status == StepStatus.RUNNING:
13
+ return " ..."
14
+ return ""
15
+
16
+
17
+ def _format_duration(seconds: float | None) -> str:
18
+ return f"[{seconds:.2f}s]" if seconds is not None else ""
19
+
20
+
21
+ def _render_step(step: Step, prefix: str, is_last: bool, lines: list[str]) -> None:
22
+ """Recursively append a step and its events/children to ``lines``."""
23
+ branch = "└─ " if is_last else "├─ "
24
+ line = f"{prefix}{branch}{step.name} {_format_duration(step.duration)}{_status_annotation(step)}"
25
+ lines.append(line)
26
+
27
+ # Continuation prefix for events and children beneath this branch.
28
+ child_prefix = prefix + (" " if is_last else "│ ")
29
+
30
+ for event in step.events:
31
+ _render_event(event, child_prefix, lines)
32
+
33
+ for i, child in enumerate(step.children):
34
+ _render_step(child, child_prefix, i == len(step.children) - 1, lines)
35
+
36
+ if step.error:
37
+ lines.append(f"{child_prefix} error: {step.error}")
38
+
39
+
40
+ def _render_event(event: Event, prefix: str, lines: list[str]) -> None:
41
+ lines.append(f"{prefix} • {event.message}")
42
+
43
+
44
+ def display_run(run: Run) -> None:
45
+ """Pretty-print a Run tree to stdout.
46
+
47
+ Renders the run name, total duration, and each step (including nested
48
+ children and structured events) as a boxed tree.
49
+ """
50
+ title = f"Run: {run.name}"
51
+ duration = _format_duration(run.duration)
52
+ if duration:
53
+ title += f" {duration}"
54
+
55
+ lines: list[str] = []
56
+
57
+ for i, step in enumerate(run.steps):
58
+ _render_step(step, " ", i == len(run.steps) - 1, lines)
59
+
60
+ for event in run.events:
61
+ lines.append(f" • {event.message}")
62
+
63
+ top = f"╔══ {title} ═══"
64
+ width = max([len(line) for line in lines] + [len(top) - 2])
65
+ bottom = "╚" + "═" * width + "╝"
66
+
67
+ print(top)
68
+ print("║")
69
+ for line in lines:
70
+ print(f"║{line}")
71
+ print(bottom)
steplot/models.py ADDED
@@ -0,0 +1,213 @@
1
+ """Data models for steplot — Event, Step, Run, and supporting types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime
7
+ from enum import Enum
8
+ from typing import Any
9
+ from uuid import uuid4
10
+
11
+
12
+ class StepStatus(Enum):
13
+ """Lifecycle status of a Step."""
14
+
15
+ RUNNING = "running"
16
+ SUCCESS = "success"
17
+ FAILED = "failed"
18
+
19
+ def __str__(self) -> str:
20
+ return self.value
21
+
22
+
23
+ @dataclass
24
+ class Event:
25
+ """A structured event emitted into a Step or Run.
26
+
27
+ Attributes:
28
+ level: Logging level (see Python's ``logging`` module).
29
+ message: Human-readable message for the event.
30
+ data: Arbitrary structured key/value payload attached to the event.
31
+ timestamp: Wall-clock time the event was emitted.
32
+ id: Stable identifier for the event.
33
+ """
34
+
35
+ level: int
36
+ message: str
37
+ data: dict[str, Any] = field(default_factory=dict)
38
+ timestamp: datetime = field(default_factory=datetime.now)
39
+ id: str = field(default_factory=lambda: uuid4().hex)
40
+
41
+ def to_dict(self) -> dict[str, Any]:
42
+ return {
43
+ "id": self.id,
44
+ "level": self.level,
45
+ "message": self.message,
46
+ "data": self.data,
47
+ "timestamp": self.timestamp.isoformat() if self.timestamp else None,
48
+ }
49
+
50
+ @classmethod
51
+ def from_dict(cls, data: dict[str, Any]) -> Event:
52
+ return cls(
53
+ id=data.get("id", uuid4().hex),
54
+ level=data.get("level", 20),
55
+ message=data["message"],
56
+ data=data.get("data", {}),
57
+ timestamp=datetime.fromisoformat(data["timestamp"]) if data.get("timestamp") else datetime.now(),
58
+ )
59
+
60
+
61
+ @dataclass
62
+ class Step:
63
+ """A named, timed unit of work within a Run.
64
+
65
+ Steps form a tree: each step may contain nested ``children`` steps as well as
66
+ structured ``events``.
67
+
68
+ Attributes:
69
+ name: Human-readable step name (often the function name).
70
+ status: Current lifecycle status.
71
+ input: Optional input value passed to the step.
72
+ output: Optional output value produced by the step.
73
+ error: Error message if the step failed.
74
+ started_at: Wall-clock time the step started.
75
+ ended_at: Wall-clock time the step finished (None while running).
76
+ children: Nested child steps.
77
+ events: Structured events emitted while the step ran.
78
+ """
79
+
80
+ name: str
81
+ status: StepStatus = StepStatus.RUNNING
82
+ input: Any = None
83
+ output: Any = None
84
+ error: str | None = None
85
+ started_at: datetime = field(default_factory=datetime.now)
86
+ ended_at: datetime | None = None
87
+ id: str = field(default_factory=lambda: uuid4().hex)
88
+ children: list[Step] = field(default_factory=list)
89
+ events: list[Event] = field(default_factory=list)
90
+
91
+ @property
92
+ def duration(self) -> float | None:
93
+ """Duration in seconds, or None while still running."""
94
+ if self.ended_at is None:
95
+ return None
96
+ return (self.ended_at - self.started_at).total_seconds()
97
+
98
+ def finish(self, status: StepStatus | None = None, **kwargs: Any) -> None:
99
+ """Mark the step as finished with the given status and optional fields."""
100
+ if self.ended_at is None:
101
+ self.ended_at = datetime.now()
102
+ if status is not None:
103
+ self.status = status
104
+ for key, value in kwargs.items():
105
+ setattr(self, key, value)
106
+
107
+ def succeed(self, output: Any = None) -> None:
108
+ self.finish(status=StepStatus.SUCCESS, output=output)
109
+
110
+ def fail(self, error: str) -> None:
111
+ self.finish(status=StepStatus.FAILED, error=error)
112
+
113
+ def add_child(self, name: str, **kwargs: Any) -> Step:
114
+ """Create and register a nested child step."""
115
+ child = Step(name=name, **kwargs)
116
+ self.children.append(child)
117
+ return child
118
+
119
+ def add_event(self, level: int, message: str, **data: Any) -> Event:
120
+ """Record a structured event on this step."""
121
+ event = Event(level=level, message=message, data=data)
122
+ self.events.append(event)
123
+ return event
124
+
125
+ def to_dict(self) -> dict[str, Any]:
126
+ return {
127
+ "id": self.id,
128
+ "name": self.name,
129
+ "status": self.status.value,
130
+ "input": self.input,
131
+ "output": self.output,
132
+ "error": self.error,
133
+ "started_at": self.started_at.isoformat() if self.started_at else None,
134
+ "ended_at": self.ended_at.isoformat() if self.ended_at else None,
135
+ "children": [c.to_dict() for c in self.children],
136
+ "events": [e.to_dict() for e in self.events],
137
+ }
138
+
139
+ @classmethod
140
+ def from_dict(cls, data: dict[str, Any]) -> Step:
141
+ return cls(
142
+ id=data.get("id", uuid4().hex),
143
+ name=data["name"],
144
+ status=StepStatus(data.get("status", StepStatus.RUNNING.value)),
145
+ input=data.get("input"),
146
+ output=data.get("output"),
147
+ error=data.get("error"),
148
+ started_at=datetime.fromisoformat(data["started_at"])
149
+ if data.get("started_at")
150
+ else datetime.now(),
151
+ ended_at=datetime.fromisoformat(data["ended_at"]) if data.get("ended_at") else None,
152
+ children=[Step.from_dict(c) for c in data.get("children", [])],
153
+ events=[Event.from_dict(e) for e in data.get("events", [])],
154
+ )
155
+
156
+
157
+ @dataclass
158
+ class Run:
159
+ """Top-level container for a tracked execution."""
160
+
161
+ id: str = field(default_factory=lambda: uuid4().hex)
162
+ name: str = "run"
163
+ started_at: datetime = field(default_factory=datetime.now)
164
+ ended_at: datetime | None = None
165
+ steps: list[Step] = field(default_factory=list)
166
+ metadata: dict[str, Any] = field(default_factory=dict)
167
+ events: list[Event] = field(default_factory=list)
168
+
169
+ @property
170
+ def duration(self) -> float | None:
171
+ if self.ended_at is None:
172
+ return None
173
+ return (self.ended_at - self.started_at).total_seconds()
174
+
175
+ def add_step(self, name: str, **kwargs: Any) -> Step:
176
+ step = Step(name=name, **kwargs)
177
+ self.steps.append(step)
178
+ return step
179
+
180
+ def add_event(self, level: int, message: str, **data: Any) -> Event:
181
+ """Record a run-level structured event (not attached to any step)."""
182
+ event = Event(level=level, message=message, data=data)
183
+ self.events.append(event)
184
+ return event
185
+
186
+ def finish(self) -> None:
187
+ if self.ended_at is None:
188
+ self.ended_at = datetime.now()
189
+
190
+ def to_dict(self) -> dict[str, Any]:
191
+ return {
192
+ "id": self.id,
193
+ "name": self.name,
194
+ "started_at": self.started_at.isoformat() if self.started_at else None,
195
+ "ended_at": self.ended_at.isoformat() if self.ended_at else None,
196
+ "steps": [s.to_dict() for s in self.steps],
197
+ "metadata": self.metadata,
198
+ "events": [e.to_dict() for e in self.events],
199
+ }
200
+
201
+ @classmethod
202
+ def from_dict(cls, data: dict[str, Any]) -> Run:
203
+ return cls(
204
+ id=data.get("id", uuid4().hex),
205
+ name=data.get("name", "run"),
206
+ started_at=datetime.fromisoformat(data["started_at"])
207
+ if data.get("started_at")
208
+ else datetime.now(),
209
+ ended_at=datetime.fromisoformat(data["ended_at"]) if data.get("ended_at") else None,
210
+ steps=[Step.from_dict(s) for s in data.get("steps", [])],
211
+ metadata=data.get("metadata", {}),
212
+ events=[Event.from_dict(e) for e in data.get("events", [])],
213
+ )
steplot/py.typed ADDED
File without changes
steplot/storage.py ADDED
@@ -0,0 +1,51 @@
1
+ """Storage for steplot runs — JSON-based."""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ from .models import Run, StepStatus
8
+
9
+
10
+ def _serialize(obj):
11
+ """JSON default serializer for values not natively JSON-serializable."""
12
+ if isinstance(obj, datetime):
13
+ return obj.isoformat()
14
+ if isinstance(obj, StepStatus):
15
+ return obj.value
16
+ return str(obj)
17
+
18
+
19
+ def save_run(run: Run, path: str = ".steplot") -> str:
20
+ """Save a Run to a JSON file.
21
+
22
+ The target is interpreted as a file path when it has a file suffix (for
23
+ example ``runs/run-1.json``), and as a directory otherwise. In the
24
+ directory case the file is written as ``{run.id}.json`` inside it. Parent
25
+ directories are created as needed.
26
+
27
+ Args:
28
+ run: The Run to persist.
29
+ path: Destination file path (with suffix) or directory.
30
+
31
+ Returns:
32
+ The absolute path of the file that was written.
33
+ """
34
+ target = Path(path)
35
+ if target.suffix:
36
+ # Treat as an explicit file path (e.g. "runs/run-1.json").
37
+ target.parent.mkdir(parents=True, exist_ok=True)
38
+ file = target
39
+ else:
40
+ # Treat as a directory; write {run.id}.json inside it.
41
+ target.mkdir(parents=True, exist_ok=True)
42
+ file = target / f"{run.id}.json"
43
+
44
+ file.write_text(json.dumps(run.to_dict(), default=_serialize, indent=2))
45
+ return str(file)
46
+
47
+
48
+ def load_run(path: str) -> Run:
49
+ """Load a Run from a JSON file path."""
50
+ data = json.loads(Path(path).read_text())
51
+ return Run.from_dict(data)
steplot/tracker.py ADDED
@@ -0,0 +1,268 @@
1
+ """Core tracking logic — @track, run_context/step_context, and log_event."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import inspect
7
+ import logging
8
+ from collections.abc import Callable, Iterator
9
+ from contextlib import contextmanager
10
+ from contextvars import ContextVar
11
+ from datetime import datetime
12
+ from typing import Any
13
+
14
+ from steplot.models import Event, Run, Step, StepStatus
15
+
16
+ # Module-level current run and current step, stored in ContextVars for async-safety
17
+ _current_run: ContextVar[Run | None] = ContextVar("current_run", default=None)
18
+ _current_step: ContextVar[Step | None] = ContextVar("current_step", default=None)
19
+
20
+ # Logger integration
21
+ _logger = logging.getLogger("steplot")
22
+
23
+
24
+ def get_current_run() -> Run | None:
25
+ """Return the active Run, or None if no run is active."""
26
+ return _current_run.get()
27
+
28
+
29
+ def get_current_step() -> Step | None:
30
+ """Return the active Step, or None if no step context is active."""
31
+ return _current_step.get()
32
+
33
+
34
+ def reset() -> None:
35
+ """Reset the global tracking state (useful between tests or runs)."""
36
+ _current_run.set(None)
37
+ _current_step.set(None)
38
+
39
+
40
+ @contextmanager
41
+ def run_context(name: str = "run", **metadata: Any) -> Iterator[Run]:
42
+ """Context manager that creates and finishes a Run.
43
+
44
+ The run is exposed via the ``run`` returned by the context; it is also made
45
+ the current run so that ``@track`` and ``log_event`` calls inside the block
46
+ are recorded on it.
47
+
48
+ Args:
49
+ name: Human-readable name for the run.
50
+ **metadata: Arbitrary key/value metadata attached to the run.
51
+
52
+ Example:
53
+ with run_context("my-agent", agent="research") as run:
54
+ do_work()
55
+ display_run(run)
56
+ """
57
+ previous_run = _current_run.get()
58
+ previous_step = _current_step.get()
59
+ run = Run(name=name, metadata=metadata)
60
+ run_token = _current_run.set(run)
61
+ step_token = _current_step.set(None)
62
+ try:
63
+ yield run
64
+ finally:
65
+ run.finish()
66
+ _current_run.reset(run_token)
67
+ _current_run.set(previous_run)
68
+ _current_step.reset(step_token)
69
+ _current_step.set(previous_step)
70
+
71
+
72
+ @contextmanager
73
+ def step_context(name: str) -> Iterator[Step]:
74
+ """Context manager that creates and finishes a Step, nesting automatically.
75
+
76
+ When used inside another step (either via ``@track`` or a parent
77
+ ``step_context``), the new step becomes a child of the current step.
78
+
79
+ Args:
80
+ name: Human-readable name for the step.
81
+
82
+ Example:
83
+ with run_context("pipeline") as run:
84
+ with step_context("fetch"):
85
+ ...
86
+ with step_context("process"):
87
+ with step_context("validate"):
88
+ ...
89
+ """
90
+ run, run_token = _ensure_run()
91
+ step = Step(name=name)
92
+
93
+ parent = _current_step.get()
94
+ if parent is not None:
95
+ parent.children.append(step)
96
+ else:
97
+ run.steps.append(step)
98
+
99
+ previous_step = _current_step.get()
100
+ step_token = _current_step.set(step)
101
+ try:
102
+ yield step
103
+ except Exception as exc: # noqa: BLE001 - always record and re-raise
104
+ step.fail(repr(exc))
105
+ raise
106
+ finally:
107
+ if step.ended_at is None:
108
+ step.succeed()
109
+ _current_step.reset(step_token)
110
+ _current_step.set(previous_step)
111
+ if run_token is not None:
112
+ run.finish()
113
+ _current_run.reset(run_token)
114
+
115
+
116
+ def log_event(level: int, message: str, **data: Any) -> Event | None:
117
+ """Emit a structured event into the current step.
118
+
119
+ If a step context is active the event is stored on that step; otherwise it
120
+ is stored on the current run. When no run exists at all the event is only
121
+ passed through to the ``steplot`` logger.
122
+
123
+ Example:
124
+ import logging
125
+ log_event(logging.INFO, "processing item", item_id=42)
126
+
127
+ Returns:
128
+ The created :class:`~steplot.models.Event`, or None if no run existed.
129
+ """
130
+ run = _current_run.get()
131
+ step = _current_step.get()
132
+ _logger.log(level, message)
133
+ if step is not None:
134
+ return step.add_event(level, message, **data)
135
+ if run is not None:
136
+ return run.add_event(level, message, **data)
137
+ return None
138
+
139
+
140
+ def track(
141
+ func: Callable | None = None,
142
+ *,
143
+ step_name: str | None = None,
144
+ capture_args: bool = True,
145
+ ) -> Callable:
146
+ """Decorator that wraps a function in a Step and records its execution.
147
+
148
+ Works with both sync and async functions.
149
+
150
+ Args:
151
+ step_name: Override the step name (defaults to the function's __name__).
152
+ capture_args: When True (default), record the positional and keyword
153
+ arguments as the step's input. Set to False to skip capturing.
154
+
155
+ Usage:
156
+ @track
157
+ def my_step():
158
+ ...
159
+
160
+ @track(step_name="custom")
161
+ async def another_step(x, y):
162
+ ...
163
+
164
+ @track(capture_args=False)
165
+ def secret_step(x):
166
+ ...
167
+ """
168
+
169
+ def decorator(fn: Callable) -> Callable:
170
+ name = step_name or fn.__name__
171
+
172
+ if inspect.iscoroutinefunction(fn):
173
+
174
+ @functools.wraps(fn)
175
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
176
+ return await _run_tracked_async(fn, name, args, kwargs, capture_args)
177
+
178
+ return async_wrapper
179
+
180
+ @functools.wraps(fn)
181
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
182
+ return _run_tracked_sync(fn, name, args, kwargs, capture_args)
183
+
184
+ return sync_wrapper
185
+
186
+ if func is not None and callable(func):
187
+ # Used as @track without arguments
188
+ return decorator(func)
189
+ return decorator
190
+
191
+
192
+ def _ensure_run() -> tuple[Run, Any | None]:
193
+ """Return the current run, creating one if needed.
194
+
195
+ Returns (run, token) where token is non-None if a run was created here.
196
+ """
197
+ run = _current_run.get()
198
+ if run is None:
199
+ run = Run()
200
+ token = _current_run.set(run)
201
+ return run, token
202
+ return run, None
203
+
204
+
205
+ def _append_step(step: Step) -> None:
206
+ """Attach a step to the current run, nesting it under the active step if any."""
207
+ parent = _current_step.get()
208
+ if parent is not None:
209
+ parent.children.append(step)
210
+ else:
211
+ run = _current_run.get()
212
+ if run is not None:
213
+ run.steps.append(step)
214
+
215
+
216
+ def _run_tracked_sync(fn: Callable, name: str, args: tuple, kwargs: dict, capture_args: bool) -> Any:
217
+ run, token = _ensure_run()
218
+ step = Step(
219
+ name=name,
220
+ input={"args": list(args), "kwargs": kwargs} if capture_args else None,
221
+ )
222
+ _append_step(step)
223
+ _emit(logging.INFO, f"→ start: {name}")
224
+ try:
225
+ result = fn(*args, **kwargs)
226
+ step.succeed(output=result)
227
+ _emit(logging.INFO, f"✓ done: {name}")
228
+ return result
229
+ except Exception as exc:
230
+ step.fail(repr(exc))
231
+ _emit(logging.ERROR, f"✗ error: {name} — {exc!r}")
232
+ if token is not None:
233
+ _current_run.reset(token)
234
+ raise
235
+ finally:
236
+ step.ended_at = datetime.now()
237
+ if token is not None and step.status == StepStatus.SUCCESS:
238
+ _current_run.reset(token)
239
+
240
+
241
+ async def _run_tracked_async(fn: Callable, name: str, args: tuple, kwargs: dict, capture_args: bool) -> Any:
242
+ run, token = _ensure_run()
243
+ step = Step(
244
+ name=name,
245
+ input={"args": list(args), "kwargs": kwargs} if capture_args else None,
246
+ )
247
+ _append_step(step)
248
+ _emit(logging.INFO, f"→ start: {name}")
249
+ try:
250
+ result = await fn(*args, **kwargs)
251
+ step.succeed(output=result)
252
+ _emit(logging.INFO, f"✓ done: {name}")
253
+ return result
254
+ except Exception as exc:
255
+ step.fail(repr(exc))
256
+ _emit(logging.ERROR, f"✗ error: {name} — {exc!r}")
257
+ if token is not None:
258
+ _current_run.reset(token)
259
+ raise
260
+ finally:
261
+ step.ended_at = datetime.now()
262
+ if token is not None and step.status == StepStatus.SUCCESS:
263
+ _current_run.reset(token)
264
+
265
+
266
+ def _emit(level: int, message: str) -> None:
267
+ """Internal helper to log without raising if no run exists."""
268
+ _logger.log(level, message)
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.5
2
+ Name: steplot
3
+ Version: 0.1.0
4
+ Summary: Lightweight observability for AI agents
5
+ Project-URL: Homepage, https://github.com/MohammadaminAlbooyeh/steplot
6
+ Project-URL: Repository, https://github.com/MohammadaminAlbooyeh/steplot
7
+ Project-URL: Issues, https://github.com/MohammadaminAlbooyeh/steplot/issues
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.12
11
+ Provides-Extra: dev
12
+ Requires-Dist: mypy>=1.10; extra == 'dev'
13
+ Requires-Dist: pytest>=8.0; extra == 'dev'
14
+ Requires-Dist: ruff>=0.5; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # steplot
18
+
19
+ Lightweight agent step tracker with decorator and logging support.
20
+
21
+ `steplot` helps you visualize and persist the execution flow of your AI agents,
22
+ scripts, or any multi-step pipeline. Wrap functions with `@track`, emit log
23
+ events with `log_event`, and render a tree of steps to the terminal or to a
24
+ JSON file.
25
+
26
+ ## Features
27
+
28
+ - `@track` decorator -- wraps any function in a timed `Step`.
29
+ - `run_context` / `step_context` -- context managers for nesting.
30
+ - `log_event` -- emit structured events into the current step.
31
+ - `display_run` -- pretty-print a run tree to the terminal.
32
+ - `save_run` / `load_run` -- persist runs as JSON.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install -e .
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ from steplot import track, run_context, display_run
44
+
45
+ @track
46
+ def research(topic: str) -> str:
47
+ return f"papers about {topic}"
48
+
49
+ @track
50
+ def summarize(papers: str) -> str:
51
+ return papers.upper()
52
+
53
+ with run_context("my-agent") as run:
54
+ papers = research("transformers")
55
+ summary = summarize(papers)
56
+
57
+ display_run(run)
58
+ ```
59
+
60
+ Output:
61
+
62
+ ```
63
+ ╔══ Run: my-agent [0.02s] ═══
64
+
65
+ ║ ├─ research [0.01s]
66
+ ║ └─ summarize [0.00s]
67
+ ╚════════════════════════════╝
68
+ ```
69
+
70
+ ## Nested steps
71
+
72
+ ```python
73
+ from steplot import run_context, step_context
74
+
75
+ with run_context("pipeline") as run:
76
+ with step_context("fetch"):
77
+ ...
78
+ with step_context("process"):
79
+ with step_context("validate"):
80
+ with step_context("score"):
81
+ ...
82
+ ```
83
+
84
+ ## Persisting runs
85
+
86
+ ```python
87
+ from steplot import save_run, load_run
88
+
89
+ save_run(run, "steplot/runs/run-1.json")
90
+ loaded = load_run("steplot/runs/run-1.json")
91
+ ```
92
+
93
+ ## API reference
94
+
95
+ ### `track`
96
+
97
+ ```python
98
+ @track
99
+ def my_step(): ...
100
+
101
+ @track(step_name="custom", capture_args=True)
102
+ def another_step(x, y): ...
103
+ ```
104
+
105
+ ### `run_context`, `step_context`
106
+
107
+ Context managers that create and automatically finish a `Run` or `Step`.
108
+
109
+ ### `log_event`
110
+
111
+ ```python
112
+ import logging
113
+ from steplot import log_event
114
+
115
+ log_event(logging.INFO, "processing item", item_id=42)
116
+ ```
117
+
118
+ ### `display_run`
119
+
120
+ Prints a tree of steps and events to stdout.
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,10 @@
1
+ steplot/__init__.py,sha256=jlcRqt0ZGEn20a_GMvy2XQr6S28J2BJBy9IMqoZDGMo,484
2
+ steplot/display.py,sha256=7PKyKEsRP5R2yqUjmtLTOQEvXM_2qO6dRO_Vl8uVWzU,2230
3
+ steplot/models.py,sha256=ufilJevaYsUTpevi1X0ge3teDjyon3we7xWmnOWiLyo,7599
4
+ steplot/storage.py,sha256=DMVtEn-fJnC07LnQrui_9pqoYqY8FGDRNZ8ceppSqlQ,1550
5
+ steplot/tracker.py,sha256=AUmd0e8HJANeUfxeu4zYfmm2wcemG6O2ii7i2NHOXEE,8145
6
+ steplot/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ steplot-0.1.0.dist-info/METADATA,sha256=arlWVxWxULznCWpYLYpwa87HxJ7PLsZy_VMtZuHnWv8,2746
8
+ steplot-0.1.0.dist-info/WHEEL,sha256=THafob7ofN-NsuMN7Mg4qZyHaQI7KkD-QlcQatYhXPo,87
9
+ steplot-0.1.0.dist-info/licenses/LICENSE,sha256=mgNwCgyMtHiCT4FEnUAFizIETCFVyhI7szPUujHYM_Y,1077
10
+ steplot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohammadamin Albooyeh
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.