@@ -86,6 +86,8 @@ async def search_events(
8686 page_id : str | None = None ,
8787 limit : int = 100 ,
8888 kind : str | None = None ,
89+ source : str | None = None ,
90+ body : str | None = None ,
8991 sort_order : EventSortOrder = EventSortOrder .TIMESTAMP ,
9092 timestamp__gte : datetime | None = None ,
9193 timestamp__lt : datetime | None = None ,
@@ -109,6 +111,15 @@ async def search_events(
109111 ):
110112 continue
111113
114+ # Apply source filter if provided
115+ if source is not None and event .source != source :
116+ continue
117+
118+ # Apply body filter if provided (case-insensitive substring match)
119+ if body is not None :
120+ if not self ._event_matches_body (event , body ):
121+ continue
122+
112123 # Apply timestamp filters if provided (ISO string comparison)
113124 if (
114125 timestamp_gte_str is not None
@@ -152,6 +163,8 @@ async def search_events(
152163 async def count_events (
153164 self ,
154165 kind : str | None = None ,
166+ source : str | None = None ,
167+ body : str | None = None ,
155168 timestamp__gte : datetime | None = None ,
156169 timestamp__lt : datetime | None = None ,
157170 ) -> int :
@@ -174,6 +187,15 @@ async def count_events(
174187 ):
175188 continue
176189
190+ # Apply source filter if provided
191+ if source is not None and event .source != source :
192+ continue
193+
194+ # Apply body filter if provided (case-insensitive substring match)
195+ if body is not None :
196+ if not self ._event_matches_body (event , body ):
197+ continue
198+
177199 # Apply timestamp filters if provided (ISO string comparison)
178200 if (
179201 timestamp_gte_str is not None
@@ -187,6 +209,32 @@ async def count_events(
187209
188210 return count
189211
212+ def _event_matches_body (self , event : Event , body : str ) -> bool :
213+ """Check if event's message content matches body filter (case-insensitive)."""
214+ # Import here to avoid circular imports
215+ from openhands .sdk .event .llm_convertible .message import MessageEvent
216+ from openhands .sdk .llm .message import content_to_str
217+
218+ # Only check MessageEvent instances for body content
219+ if not isinstance (event , MessageEvent ):
220+ return False
221+
222+ # Extract text content from the message
223+ text_parts = content_to_str (event .llm_message .content )
224+
225+ # Also check extended content if present
226+ if event .extended_content :
227+ extended_text_parts = content_to_str (event .extended_content )
228+ text_parts .extend (extended_text_parts )
229+
230+ # Also check reasoning content if present
231+ if event .reasoning_content :
232+ text_parts .append (event .reasoning_content )
233+
234+ # Combine all text content and perform case-insensitive substring match
235+ full_text = " " .join (text_parts ).lower ()
236+ return body .lower () in full_text
237+
190238 async def batch_get_events (self , event_ids : list [str ]) -> list [Event | None ]:
191239 """Given a list of ids, get events (Or none for any which were not found)"""
192240 results = await asyncio .gather (
0 commit comments