1010import threading
1111from collections .abc import Callable
1212from pathlib import Path
13- from typing import Any
13+ from typing import Any , Protocol
1414
1515import requests
1616
1717from sentience .tracing import TraceSink
1818
1919
20+ class SentienceLogger (Protocol ):
21+ """Protocol for optional logger interface."""
22+
23+ def info (self , message : str ) -> None :
24+ """Log info message."""
25+ ...
26+
27+ def warning (self , message : str ) -> None :
28+ """Log warning message."""
29+ ...
30+
31+ def error (self , message : str ) -> None :
32+ """Log error message."""
33+ ...
34+
35+
2036class CloudTraceSink (TraceSink ):
2137 """
2238 Enterprise Cloud Sink: "Local Write, Batch Upload" pattern.
@@ -51,17 +67,30 @@ class CloudTraceSink(TraceSink):
5167 >>> tracer.close(blocking=False) # Returns immediately
5268 """
5369
54- def __init__ (self , upload_url : str , run_id : str ):
70+ def __init__ (
71+ self ,
72+ upload_url : str ,
73+ run_id : str ,
74+ api_key : str | None = None ,
75+ api_url : str | None = None ,
76+ logger : SentienceLogger | None = None ,
77+ ):
5578 """
5679 Initialize cloud trace sink.
5780
5881 Args:
5982 upload_url: Pre-signed PUT URL from Sentience API
6083 (e.g., "https://sentience.nyc3.digitaloceanspaces.com/...")
6184 run_id: Unique identifier for this agent run (used for persistent cache)
85+ api_key: Sentience API key for calling /v1/traces/complete
86+ api_url: Sentience API base URL (default: https://api.sentienceapi.com)
87+ logger: Optional logger instance for logging file sizes and errors
6288 """
6389 self .upload_url = upload_url
6490 self .run_id = run_id
91+ self .api_key = api_key
92+ self .api_url = api_url or "https://api.sentienceapi.com"
93+ self .logger = logger
6594
6695 # Use persistent cache directory instead of temp file
6796 # This ensures traces survive process crashes
@@ -74,6 +103,10 @@ def __init__(self, upload_url: str, run_id: str):
74103 self ._closed = False
75104 self ._upload_successful = False
76105
106+ # File size tracking (NEW)
107+ self .trace_file_size_bytes = 0
108+ self .screenshot_total_size_bytes = 0
109+
77110 def emit (self , event : dict [str , Any ]) -> None :
78111 """
79112 Write event to local persistent file (Fast, non-blocking).
@@ -140,6 +173,18 @@ def _do_upload(self, on_progress: Callable[[int, int], None] | None = None) -> N
140173 compressed_data = gzip .compress (trace_data )
141174 compressed_size = len (compressed_data )
142175
176+ # Measure trace file size (NEW)
177+ self .trace_file_size_bytes = compressed_size
178+
179+ # Log file sizes if logger is provided (NEW)
180+ if self .logger :
181+ self .logger .info (
182+ f"Trace file size: { self .trace_file_size_bytes / 1024 / 1024 :.2f} MB"
183+ )
184+ self .logger .info (
185+ f"Screenshot total: { self .screenshot_total_size_bytes / 1024 / 1024 :.2f} MB"
186+ )
187+
143188 # Report progress: start
144189 if on_progress :
145190 on_progress (0 , compressed_size )
@@ -165,6 +210,9 @@ def _do_upload(self, on_progress: Callable[[int, int], None] | None = None) -> N
165210 if on_progress :
166211 on_progress (compressed_size , compressed_size )
167212
213+ # Call /v1/traces/complete to report file sizes (NEW)
214+ self ._complete_trace ()
215+
168216 # Delete file only on successful upload
169217 if os .path .exists (self ._path ):
170218 try :
@@ -183,6 +231,44 @@ def _do_upload(self, on_progress: Callable[[int, int], None] | None = None) -> N
183231 print (f" Local trace preserved at: { self ._path } " )
184232 # Don't raise - preserve trace locally even if upload fails
185233
234+ def _complete_trace (self ) -> None :
235+ """
236+ Call /v1/traces/complete to report file sizes to gateway.
237+
238+ This is a best-effort call - failures are logged but don't affect upload success.
239+ """
240+ if not self .api_key :
241+ # No API key - skip complete call
242+ return
243+
244+ try :
245+ response = requests .post (
246+ f"{ self .api_url } /v1/traces/complete" ,
247+ headers = {"Authorization" : f"Bearer { self .api_key } " },
248+ json = {
249+ "run_id" : self .run_id ,
250+ "stats" : {
251+ "trace_file_size_bytes" : self .trace_file_size_bytes ,
252+ "screenshot_total_size_bytes" : self .screenshot_total_size_bytes ,
253+ },
254+ },
255+ timeout = 10 ,
256+ )
257+
258+ if response .status_code == 200 :
259+ if self .logger :
260+ self .logger .info ("Trace completion reported to gateway" )
261+ else :
262+ if self .logger :
263+ self .logger .warning (
264+ f"Failed to report trace completion: HTTP { response .status_code } "
265+ )
266+
267+ except Exception as e :
268+ # Best-effort - log but don't fail
269+ if self .logger :
270+ self .logger .warning (f"Error reporting trace completion: { e } " )
271+
186272 def __enter__ (self ):
187273 """Context manager support."""
188274 return self
0 commit comments