saccade 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.
saccade-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,382 @@
1
+ Metadata-Version: 2.4
2
+ Name: saccade
3
+ Version: 0.1.0
4
+ Summary: Saccade: Tracing and observability library for AI agents
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: pydantic>=2.4
8
+ Requires-Dist: python-ulid>=2.4
9
+
10
+ # Saccade
11
+
12
+ A tracing and observability library for AI agents with built-in metrics.
13
+
14
+ **Status:** v0.1.0 | **Tests:** 204 passing
15
+
16
+ ---
17
+
18
+ ## What is Saccade?
19
+
20
+ Saccade provides primitives for tracing agent execution, capturing metrics (tokens, cost, latency), and analyzing execution patterns through flexible projections.
21
+
22
+ **Core Philosophy:**
23
+ - Event-driven architecture with immutable events
24
+ - Zero-config observability via `Span` context managers
25
+ - Multiple views of the same trace (tree, graph, cost, state, timeline)
26
+ - Built-in metric tracking for tokens, cost, and latency
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install saccade
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ### Basic Tracing
37
+
38
+ ```python
39
+ from saccade import Trace, Span, project_tree
40
+
41
+ with Trace() as trace:
42
+ with Span("agent", kind="agent") as agent:
43
+ result = do_some_work()
44
+ agent.set_output(result)
45
+
46
+ tree = project_tree(trace.events)
47
+ print(f"Total tokens: {tree.total_tokens.input}")
48
+ ```
49
+
50
+ ### Streaming with Metrics
51
+
52
+ ```python
53
+ from saccade import Trace, Span, TokenMetrics, CostMetrics, project_cost
54
+
55
+ with Trace() as trace:
56
+ with Span("llm_call", kind="llm") as llm:
57
+ # Simulate streaming
58
+ for chunk in ["Hello", " ", "world"]:
59
+ llm.stream(chunk)
60
+
61
+ llm.set_output("Hello world")
62
+ llm.set_metrics(
63
+ tokens=TokenMetrics(input=50, output=4),
64
+ cost=CostMetrics(usd=0.002)
65
+ )
66
+
67
+ cost_view = project_cost(trace.events)
68
+ print(f"Cost: ${cost_view.total_cost.usd}")
69
+ ```
70
+
71
+ ### Nested Spans and Relations
72
+
73
+ ```python
74
+ from saccade import Trace, Span, project_tree
75
+
76
+ with Trace() as trace:
77
+ with Span("agent", kind="agent") as agent:
78
+ with Span("planning", kind="llm") as planning:
79
+ planning.set_output("Plan created")
80
+
81
+ with Span("tool_execution", kind="tool") as tool:
82
+ tool.set_output("Tool result")
83
+
84
+ tree = project_tree(trace.events)
85
+
86
+ # Traverse tree
87
+ for root in tree.roots:
88
+ print(f"Span: {root.name}")
89
+ for child in root.children:
90
+ print(f" └─ {child.name}")
91
+ ```
92
+
93
+ ### Real-Time Event Streaming
94
+
95
+ ```python
96
+ from saccade import Trace, Span, EventType
97
+
98
+ with Trace() as trace:
99
+ # Subscribe to live events
100
+ def on_event(event):
101
+ if event.type == EventType.CHUNK:
102
+ print(f"[STREAMING] {event.chunk}")
103
+ elif event.type == EventType.ERROR:
104
+ print(f"[ERROR] {event.error}")
105
+
106
+ trace.subscribe(on_event)
107
+
108
+ with Span("llm", kind="llm") as llm:
109
+ for chunk in ["Hello", " world"]:
110
+ llm.stream(chunk)
111
+ ```
112
+
113
+ ## Public API
114
+
115
+ ### Core Classes
116
+
117
+ | Class | Description |
118
+ |-------|-------------|
119
+ | `Trace` | Entry point for tracing. Creates a `TraceBus` and manages context. |
120
+ | `Span` | Context manager for tracing operations. Emits events, tracks metrics. |
121
+ | `TraceBus` | Collects events and notifies subscribers. (Internal, accessible via `saccade.primitives`) |
122
+
123
+ ### Event Types
124
+
125
+ | Type | Description |
126
+ |------|-------------|
127
+ | `TraceEvent` | Immutable record of a state change in a span. |
128
+ | `EventType` | Enum of event types: `START`, `CHUNK`, `OUTPUT`, `SUCCESS`, `ERROR`, `CANCEL` |
129
+ | `Relation` | Enum of relation types: `CONTEXT`, `DATAFLOW` |
130
+
131
+ ### Metric Types
132
+
133
+ All metric types support addition (`+`) for aggregation:
134
+
135
+ | Type | Fields |
136
+ |------|--------|
137
+ | `TokenMetrics` | `input`, `output`, `reasoning`, `cached`, `cache_write` |
138
+ | `CostMetrics` | `usd` (Decimal) |
139
+ | `LatencyMetrics` | `total_ms`, `time_to_first_token_ms`, `has_clock_skew` |
140
+ | `OperationMeta` | `model`, `provider`, `host`, `kind`, `correlation_id` |
141
+
142
+ ### Projectors
143
+
144
+ Transform events into different views:
145
+
146
+ | Function | Returns | Description |
147
+ |----------|----------|-------------|
148
+ | `project_tree(events)` | `TreeView` | Hierarchical tree using "context" relations |
149
+ | `project_graph(events)` | `GraphView` | Directed graph with all relations |
150
+ | `project_cost(events)` | `CostView` | Aggregated cost and token metrics |
151
+ | `project_state(events, at_timestamp)` | `StateView` | Snapshot of state at a specific time |
152
+ | `project_timeline(events)` | `TimelineView` | Chronological view with temporal grouping |
153
+
154
+ ## Projector Usage Examples
155
+
156
+ ### Tree View
157
+
158
+ ```python
159
+ from saccade import Trace, Span, project_tree
160
+
161
+ with Trace() as trace:
162
+ with Span("parent") as p:
163
+ with Span("child1"):
164
+ pass
165
+ with Span("child2"):
166
+ pass
167
+
168
+ tree = project_tree(trace.events)
169
+ root = tree.roots[0]
170
+
171
+ print(f"Root: {root.name}")
172
+ print(f"Children: {[c.name for c in root.children]}")
173
+ print(f"Total tokens: {tree.total_tokens.input}")
174
+ print(f"Peak context: {tree.peak_context}")
175
+ ```
176
+
177
+ ### Graph View
178
+
179
+ ```python
180
+ from saccade import Trace, Span, Relation, project_graph
181
+
182
+ with Trace() as trace:
183
+ with Span("a") as a:
184
+ pass
185
+
186
+ with Span("b") as b:
187
+ b.relate("dataflow", a.id)
188
+
189
+ graph = project_graph(trace.events)
190
+
191
+ # Find nodes by name
192
+ node_a = graph.find_by_name("a")
193
+ node_b = graph.find_by_name("b")
194
+
195
+ # Get edges by type
196
+ dataflow_edges = graph.edges_by_type(Relation.DATAFLOW)
197
+ ```
198
+
199
+ ### Cost View
200
+
201
+ ```python
202
+ from saccade import Trace, Span, TokenMetrics, CostMetrics, project_cost
203
+
204
+ with Trace() as trace:
205
+ with Span("llm1") as s1:
206
+ s1.set_metrics(
207
+ tokens=TokenMetrics(input=100, output=20),
208
+ cost=CostMetrics(usd=0.01)
209
+ )
210
+
211
+ with Span("llm2") as s2:
212
+ s2.set_metrics(
213
+ tokens=TokenMetrics(input=50, output=10),
214
+ cost=CostMetrics(usd=0.005)
215
+ )
216
+
217
+ cost = project_cost(trace.events)
218
+ print(f"Total cost: ${cost.total_cost.usd}")
219
+ print(f"Total input tokens: {cost.total_tokens.input}")
220
+ print(f"Cost per 1k input: ${cost.cost_per_1k_input}")
221
+ ```
222
+
223
+ ### Timeline View
224
+
225
+ ```python
226
+ from saccade import Trace, Span, project_timeline
227
+
228
+ with Trace() as trace:
229
+ with Span("a"):
230
+ pass
231
+ with Span("b"):
232
+ pass
233
+
234
+ timeline = project_timeline(trace.events)
235
+
236
+ # Group by time windows (e.g., 1 second)
237
+ for bucket in timeline.by_seconds(1.0):
238
+ print(f"Time {bucket.start_time}-{bucket.end_time}: {len(bucket.events)} events")
239
+ ```
240
+
241
+ ### State View (Snapshot)
242
+
243
+ ```python
244
+ from saccade import Trace, Span, project_state
245
+ import time
246
+
247
+ with Trace() as trace:
248
+ with Span("a"):
249
+ pass
250
+
251
+ mid_point = time.time()
252
+
253
+ with Span("b"):
254
+ pass
255
+
256
+ # Snapshot at mid_point - only sees "a"
257
+ state = project_state(trace.events, at_timestamp=mid_point)
258
+ print(f"Active spans at snapshot: {[s.name for s in state.active_spans]}")
259
+ ```
260
+
261
+ ## Advanced Usage
262
+
263
+ ### Custom Relations
264
+
265
+ ```python
266
+ from saccade import Trace, Span, project_graph
267
+
268
+ with Trace() as trace:
269
+ with Span("task_a") as a:
270
+ pass
271
+
272
+ with Span("task_b") as b:
273
+ # Declare dependency
274
+ b.relate("depends_on", a.id)
275
+
276
+ graph = project_graph(trace.events)
277
+ depends_edges = graph.edges_by_type("depends_on")
278
+ ```
279
+
280
+ ### Span Kinds
281
+
282
+ Saccade provides constants for common span kinds:
283
+
284
+ ```python
285
+ from saccade import Span, SpanKind
286
+
287
+ with Span("agent", kind=SpanKind.AGENT):
288
+ pass
289
+
290
+ with Span("llm", kind=SpanKind.LLM):
291
+ pass
292
+
293
+ with Span("tool", kind=SpanKind.TOOL):
294
+ pass
295
+ ```
296
+
297
+ Available kinds:
298
+ - `SpanKind.AGENT` - Agent execution
299
+ - `SpanKind.LLM` - LLM generation
300
+ - `SpanKind.TOOL` - Tool execution
301
+ - `SpanKind.EMBEDDING` - Embedding generation
302
+ - `SpanKind.RETRIEVAL` - Retrieval operation
303
+
304
+ ### Error Handling
305
+
306
+ ```python
307
+ from saccade import Trace, Span, EventType
308
+
309
+ with Trace() as trace:
310
+ trace.subscribe(lambda e: print(f"{e.type}: {e.name}"))
311
+
312
+ try:
313
+ with Span("failing_span"):
314
+ raise ValueError("Something went wrong")
315
+ except ValueError:
316
+ pass
317
+
318
+ # Events captured:
319
+ # 1. START
320
+ # 2. ERROR (with error message)
321
+ # 3. Latency included on error event
322
+ ```
323
+
324
+ ### Partial Results on Error
325
+
326
+ When a span fails, if `set_output()` was called before the error, the output is preserved:
327
+
328
+ ```python
329
+ with Span("partial_success") as s:
330
+ s.set_output("Partial result")
331
+ # More work that fails
332
+ raise ValueError("Failed")
333
+
334
+ # ERROR event is emitted, but OUTPUT event comes first
335
+ # Projection will show the partial output
336
+ ```
337
+
338
+ ## Testing
339
+
340
+ ```bash
341
+ # Run all tests
342
+ pytest tests/
343
+
344
+ # Run with coverage
345
+ pytest --cov=src --cov-report=html
346
+
347
+ # Run specific marker groups
348
+ pytest -m unit
349
+ pytest -m integration
350
+ pytest -m e2e
351
+ ```
352
+
353
+ ## Documentation
354
+
355
+ - **[TYPES.md](docs/TYPES.md)** - Complete type specification
356
+ - **[DECISIONS.md](docs/DECISIONS.md)** - Architectural decisions
357
+ - **[DESIGN_VALIDATION.md](docs/DESIGN_VALIDATION.md)** - Design validation scenarios
358
+
359
+ ## Design Principles
360
+
361
+ 1. **Event-First**: State is derived purely from an append-only log of immutable `TraceEvent`s
362
+ 2. **Generic Relations**: Spans declare relationships as `{type: [span_ids]}`. The projector interprets them
363
+ 3. **Auto-Captured Context**: The "context" relation is automatically added from execution stack
364
+ 4. **Synchronous Emission**: Events are emitted immediately to subscribers (no queue, no background tasks)
365
+ 5. **User Manages Lifecycle**: Memory and persistence are the user's responsibility
366
+
367
+ ## Limitations
368
+
369
+ | Limitation | Workaround |
370
+ |------------|------------|
371
+ | Single-process only | Use OpenTelemetry for distributed tracing |
372
+ | Not thread-safe | Use asyncio only |
373
+ | In-memory only | Export events periodically: `[e.model_dump() for e in trace.events]` |
374
+ | No built-in persistence | Serialize to JSON, save to database, etc. |
375
+
376
+ ## License
377
+
378
+ MIT
379
+
380
+ ## Contributing
381
+
382
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.