trodo-python 2.1.0__py3-none-any.whl → 2.2.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.
trodo/__init__.py CHANGED
@@ -40,7 +40,7 @@ Downstream microservice (join the caller's run instead of making a new one):
40
40
 
41
41
  from __future__ import annotations
42
42
 
43
- __version__ = "2.1.0"
43
+ __version__ = "2.2.0"
44
44
 
45
45
  from typing import Any, Callable, Dict, List, Optional, Union
46
46
 
@@ -85,6 +85,8 @@ __all__ = [
85
85
  "llm",
86
86
  "retrieval",
87
87
  "join_run",
88
+ "start_run",
89
+ "end_run",
88
90
  "track_llm_call",
89
91
  "feedback",
90
92
  "get_tracer",
@@ -264,6 +266,52 @@ def join_run(
264
266
  )
265
267
 
266
268
 
269
+ def start_run(
270
+ agent_name: str,
271
+ *,
272
+ run_id: Optional[str] = None,
273
+ distinct_id: Optional[str] = None,
274
+ conversation_id: Optional[str] = None,
275
+ parent_run_id: Optional[str] = None,
276
+ metadata: Optional[Dict[str, Any]] = None,
277
+ input: Any = None,
278
+ ) -> str:
279
+ """Open a Run record without a context manager.
280
+
281
+ Pairs with :func:`end_run` for sessions that span multiple processes or
282
+ HTTP requests. Returns the run_id (caller-supplied or freshly minted).
283
+ Between start_run and end_run any process can use ``join_run(run_id, ...)``
284
+ to add spans.
285
+ """
286
+ return _get_client().start_run(
287
+ agent_name,
288
+ run_id=run_id,
289
+ distinct_id=distinct_id,
290
+ conversation_id=conversation_id,
291
+ parent_run_id=parent_run_id,
292
+ metadata=metadata,
293
+ input=input,
294
+ )
295
+
296
+
297
+ def end_run(
298
+ run_id: str,
299
+ *,
300
+ output: Any = None,
301
+ status: str = "ok",
302
+ error_summary: Optional[str] = None,
303
+ metadata: Optional[Dict[str, Any]] = None,
304
+ ) -> None:
305
+ """Finalise a Run opened by :func:`start_run`."""
306
+ _get_client().end_run(
307
+ run_id,
308
+ output=output,
309
+ status=status,
310
+ error_summary=error_summary,
311
+ metadata=metadata,
312
+ )
313
+
314
+
267
315
  def tool(
268
316
  name: Any = None,
269
317
  fn: Optional[Callable[..., Any]] = None,
trodo/client.py CHANGED
@@ -17,6 +17,8 @@ from .otel.wrap_agent import (
17
17
  wrap_agent as wrap_agent_ctx,
18
18
  span as span_ctx,
19
19
  join_run as join_run_ctx,
20
+ start_run as start_run_fn,
21
+ end_run as end_run_fn,
20
22
  current_run_id as _current_run_id,
21
23
  current_span_id as _current_span_id,
22
24
  )
@@ -284,6 +286,51 @@ class TrodoClient:
284
286
  """Create a nested span inside the current run."""
285
287
  return span_ctx(name=name, kind=kind, input=input, attributes=attributes)
286
288
 
289
+ def start_run(
290
+ self,
291
+ agent_name: str,
292
+ *,
293
+ run_id: Optional[str] = None,
294
+ distinct_id: Optional[str] = None,
295
+ conversation_id: Optional[str] = None,
296
+ parent_run_id: Optional[str] = None,
297
+ metadata: Optional[Dict[str, Any]] = None,
298
+ input: Any = None,
299
+ ) -> str:
300
+ """Open a Run record outside a context manager. Returns the run_id.
301
+
302
+ Use ``end_run`` to finalise, ``join_run`` from any process to add spans.
303
+ """
304
+ return start_run_fn(
305
+ processor=self._span_processor,
306
+ agent_name=agent_name,
307
+ run_id=run_id,
308
+ distinct_id=distinct_id,
309
+ conversation_id=conversation_id,
310
+ parent_run_id=parent_run_id,
311
+ metadata=metadata,
312
+ input=input,
313
+ )
314
+
315
+ def end_run(
316
+ self,
317
+ run_id: str,
318
+ *,
319
+ output: Any = None,
320
+ status: str = "ok",
321
+ error_summary: Optional[str] = None,
322
+ metadata: Optional[Dict[str, Any]] = None,
323
+ ) -> None:
324
+ """Finalise a Run previously opened by :meth:`start_run`."""
325
+ end_run_fn(
326
+ run_id,
327
+ processor=self._span_processor,
328
+ output=output,
329
+ status=status,
330
+ error_summary=error_summary,
331
+ metadata=metadata,
332
+ )
333
+
287
334
  def join_run(
288
335
  self,
289
336
  run_id: str,
trodo/otel/processor.py CHANGED
@@ -136,6 +136,25 @@ class TrodoSpanProcessor:
136
136
  except Exception:
137
137
  pass
138
138
 
139
+ def start_run(self, run: TrodoRun) -> None:
140
+ """Open a Run row server-side without holding a context manager.
141
+
142
+ Pairs with ``end_run`` for sessions that span multiple processes or
143
+ HTTP requests. Spans emitted between start_run and end_run flush
144
+ incrementally via append_spans (callers are expected to mark_joined).
145
+ """
146
+ try:
147
+ self._http.post_run_start({"run": run.to_dict()})
148
+ except Exception:
149
+ pass
150
+
151
+ def end_run(self, run_id: str, payload: Dict[str, Any]) -> None:
152
+ """Finalise a Run opened by start_run."""
153
+ try:
154
+ self._http.post_run_end(run_id, payload)
155
+ except Exception:
156
+ pass
157
+
139
158
  def append_spans(self, run_id: str, spans: List[TrodoSpan]) -> None:
140
159
  """Stream spans for a long-running or joined run without finalising."""
141
160
  if not spans:
trodo/otel/wrap_agent.py CHANGED
@@ -163,6 +163,76 @@ class SpanHandle:
163
163
  self.tool_name = tool_name
164
164
 
165
165
 
166
+ def start_run(
167
+ *,
168
+ processor: TrodoSpanProcessor,
169
+ agent_name: str,
170
+ run_id: Optional[str] = None,
171
+ distinct_id: Optional[str] = None,
172
+ conversation_id: Optional[str] = None,
173
+ parent_run_id: Optional[str] = None,
174
+ metadata: Optional[Dict[str, Any]] = None,
175
+ input: Any = None,
176
+ ) -> str:
177
+ """Open a Run record without holding a context manager.
178
+
179
+ Pairs with :func:`end_run` for sessions that span multiple processes or
180
+ HTTP requests (e.g. an MCP server where ``initialize`` opens a run and
181
+ later ``tools/call`` requests append spans before a final close).
182
+
183
+ Returns the ``run_id`` (caller-supplied or freshly minted UUID). Between
184
+ ``start_run`` and ``end_run`` any process can use ``join_run(run_id, ...)``
185
+ to add spans — they flush incrementally via ``append_spans``.
186
+ """
187
+ rid = run_id or str(uuid.uuid4())
188
+ run = TrodoRun(
189
+ run_id=rid,
190
+ agent_name=agent_name,
191
+ distinct_id=distinct_id,
192
+ conversation_id=conversation_id,
193
+ parent_run_id=parent_run_id,
194
+ status="running",
195
+ input=_truncate(input),
196
+ started_at=_now_iso(),
197
+ metadata=metadata,
198
+ )
199
+ processor.mark_joined(rid)
200
+ processor.start_run(run)
201
+ return rid
202
+
203
+
204
+ def end_run(
205
+ run_id: str,
206
+ *,
207
+ processor: TrodoSpanProcessor,
208
+ output: Any = None,
209
+ status: str = "ok",
210
+ error_summary: Optional[str] = None,
211
+ metadata: Optional[Dict[str, Any]] = None,
212
+ ) -> None:
213
+ """Finalise a Run opened by :func:`start_run`.
214
+
215
+ Aggregates any locally-buffered spans for ``run_id``, POSTs to the
216
+ ``/runs/{id}/end`` endpoint, and unmarks the run as joined. Idempotent
217
+ on the local-state side; the backend treats a second call as a row update.
218
+ """
219
+ pending = processor.get_pending(run_id)
220
+ agg = _aggregate(pending)
221
+ payload: Dict[str, Any] = {
222
+ "ended_at": _now_iso(),
223
+ "status": status,
224
+ **agg,
225
+ }
226
+ if output is not None:
227
+ payload["output"] = _truncate(output)
228
+ if error_summary is not None:
229
+ payload["error_summary"] = _truncate(error_summary, 4_000)
230
+ if metadata is not None:
231
+ payload["metadata"] = metadata
232
+ processor.end_run(run_id, payload)
233
+ processor.unmark_joined(run_id)
234
+
235
+
166
236
  class wrap_agent:
167
237
  """Context manager wrapping an agent run.
168
238
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Trodo Analytics SDK for Python — server-side event tracking
5
5
  License: ISC
6
6
  Keywords: analytics,tracking,trodo,server-side
@@ -263,6 +263,34 @@ with trodo.join_run(
263
263
  ...
264
264
  ```
265
265
 
266
+ ### Long-lived sessions across processes — `start_run` / `end_run`
267
+
268
+ `wrap_agent` is a context manager — it opens *and* closes the run in one
269
+ call stack. For sessions that live across many HTTP requests (an MCP
270
+ server, a websocket-pinned chat, scheduled jobs that resume on different
271
+ workers), use `start_run` to open the run from one process and `end_run`
272
+ to finalise it later. Between the two, any process can use `join_run` to
273
+ add child spans. Same `run_id` threads through everything.
274
+
275
+ ```python
276
+ # Process A — open the run for an MCP session.
277
+ run_id = trodo.start_run(
278
+ 'external_mcp_session',
279
+ distinct_id=str(user_id),
280
+ conversation_id=mcp_session_id,
281
+ )
282
+ redis.set(f"mcp:run:{mcp_session_id}", run_id, ex=3600)
283
+
284
+ # Process B (later, possibly a different worker) — append a tool span.
285
+ run_id = redis.get(f"mcp:run:{mcp_session_id}").decode()
286
+ with trodo.join_run(run_id, name='tool.run_funnel_query', kind='tool') as span:
287
+ span.set_input(args)
288
+ span.set_output(result)
289
+
290
+ # When the session ends (timeout sweeper, explicit close):
291
+ trodo.end_run(run_id, status='ok')
292
+ ```
293
+
266
294
  ### Conversation binding & feedback
267
295
 
268
296
  ```python
@@ -1,5 +1,5 @@
1
- trodo/__init__.py,sha256=dQ7g0ETsOiDsrgrF9fBV7PCNplmc49Ffrha8RG3zBvo,11603
2
- trodo/client.py,sha256=0D_1AXpIQYThwZ_Y03CpfGyhblHTsRYiWIKY3JkAuZE,14092
1
+ trodo/__init__.py,sha256=lo_QFNEk_4ylQ_4-v0RBfdA8ed30M5QjlA7ylJAiAjU,12870
2
+ trodo/client.py,sha256=x_HjIyLMowUU-w73GYETEZWWHKjWBC-4LpG6lfLBRmU,15499
3
3
  trodo/types.py,sha256=eySgUvCXROG2TxtxgiU0MNr5iH0DEcduK8bmYtTKG44,3138
4
4
  trodo/user_context.py,sha256=9la6azzwEanVmdP4ps_xMoufbeWVeIGU-M8ychmgajg,7859
5
5
  trodo/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -15,15 +15,15 @@ trodo/otel/__init__.py,sha256=yiRFXWUU45bAM2CV37XeO7zf1hmnmjufdP4XO50yEyE,624
15
15
  trodo/otel/auto_instrument.py,sha256=J_neFxvO-3YACUvtetY4RdM8xYA_79SZUgPry6hXrm8,9434
16
16
  trodo/otel/context.py,sha256=iJ1rE42-SbO8VZHAxhIl2ZJXgNwLIVps5xLg8GKgfFc,1165
17
17
  trodo/otel/helpers.py,sha256=cvgFrdT8yP92P9mttloiHPr_eTCe8cC4NVrxrJo_I-A,13234
18
- trodo/otel/processor.py,sha256=AHEfNGmSG0nrdUDgb__2_7PdBZM4sij26IugUN-E4Ko,5576
19
- trodo/otel/wrap_agent.py,sha256=xdJ2C6GrNaL38hy29m5QAprqcCJWJRlEAW9GHB765I0,15237
18
+ trodo/otel/processor.py,sha256=jVZkslZlw50G5uRAa7-GMRgn_yvae58EmlWTZL8tMkQ,6285
19
+ trodo/otel/wrap_agent.py,sha256=mwHYwxtg9A9VUBdlbCKLswrNP0v5oY8ELpvt1JVYE8Q,17495
20
20
  trodo/queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  trodo/queue/batch_flusher.py,sha256=4Lg6T3Urwi9U0Q4FpFGPmjDYKg4ZliCTR-ND8BJvWaY,1298
22
22
  trodo/queue/event_queue.py,sha256=EVFZrhlq_kwC3jJ2GK0wMhHISf9UzLCZNDnT_aZ2I2A,872
23
23
  trodo/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  trodo/session/server_session.py,sha256=uBAq1QSYPUUaHFSeoOyM5Yr65dLb8T82OOx3D1BrdrE,1970
25
25
  trodo/session/session_manager.py,sha256=JrgH1VeicmtlxPR4dXEuJbxhi23OelkgwW3-9Slv80o,2525
26
- trodo_python-2.1.0.dist-info/METADATA,sha256=GMxU5fOOj4H2cXu9hV0QPER4XtnMXyyWvLmtMLFWpd8,15259
27
- trodo_python-2.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
28
- trodo_python-2.1.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
29
- trodo_python-2.1.0.dist-info/RECORD,,
26
+ trodo_python-2.2.0.dist-info/METADATA,sha256=2WsoZx5P03S5LG7JDUsCfBwTLVGSCVYa3R8ZS8KKLqw,16353
27
+ trodo_python-2.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
28
+ trodo_python-2.2.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
29
+ trodo_python-2.2.0.dist-info/RECORD,,