letta-nightly 0.11.0.dev20250807104511__py3-none-any.whl → 0.11.0.dev20250808104456__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.
Files changed (30) hide show
  1. letta/agent.py +2 -1
  2. letta/agents/letta_agent.py +215 -143
  3. letta/constants.py +4 -1
  4. letta/embeddings.py +6 -5
  5. letta/functions/function_sets/base.py +2 -2
  6. letta/functions/function_sets/files.py +22 -9
  7. letta/interfaces/anthropic_streaming_interface.py +291 -265
  8. letta/interfaces/openai_streaming_interface.py +270 -250
  9. letta/llm_api/anthropic.py +3 -10
  10. letta/llm_api/openai_client.py +6 -1
  11. letta/orm/__init__.py +1 -0
  12. letta/orm/step.py +14 -0
  13. letta/orm/step_metrics.py +71 -0
  14. letta/schemas/enums.py +9 -0
  15. letta/schemas/llm_config.py +8 -6
  16. letta/schemas/providers/lmstudio.py +2 -2
  17. letta/schemas/providers/ollama.py +42 -54
  18. letta/schemas/providers/openai.py +1 -1
  19. letta/schemas/step.py +6 -0
  20. letta/schemas/step_metrics.py +23 -0
  21. letta/schemas/tool_rule.py +10 -29
  22. letta/services/step_manager.py +179 -1
  23. letta/services/tool_executor/builtin_tool_executor.py +4 -1
  24. letta/services/tool_executor/core_tool_executor.py +2 -10
  25. letta/services/tool_executor/files_tool_executor.py +89 -40
  26. {letta_nightly-0.11.0.dev20250807104511.dist-info → letta_nightly-0.11.0.dev20250808104456.dist-info}/METADATA +1 -1
  27. {letta_nightly-0.11.0.dev20250807104511.dist-info → letta_nightly-0.11.0.dev20250808104456.dist-info}/RECORD +30 -28
  28. {letta_nightly-0.11.0.dev20250807104511.dist-info → letta_nightly-0.11.0.dev20250808104456.dist-info}/LICENSE +0 -0
  29. {letta_nightly-0.11.0.dev20250807104511.dist-info → letta_nightly-0.11.0.dev20250808104456.dist-info}/WHEEL +0 -0
  30. {letta_nightly-0.11.0.dev20250807104511.dist-info → letta_nightly-0.11.0.dev20250808104456.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  from datetime import datetime
2
2
  from enum import Enum
3
- from typing import List, Literal, Optional
3
+ from typing import Dict, List, Literal, Optional
4
4
 
5
5
  from sqlalchemy import select
6
6
  from sqlalchemy.ext.asyncio import AsyncSession
@@ -12,6 +12,7 @@ from letta.orm.job import Job as JobModel
12
12
  from letta.orm.sqlalchemy_base import AccessType
13
13
  from letta.orm.step import Step as StepModel
14
14
  from letta.otel.tracing import get_trace_id, trace_method
15
+ from letta.schemas.enums import StepStatus
15
16
  from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
16
17
  from letta.schemas.openai.chat_completion_response import UsageStatistics
17
18
  from letta.schemas.step import Step as PydanticStep
@@ -87,6 +88,10 @@ class StepManager:
87
88
  job_id: Optional[str] = None,
88
89
  step_id: Optional[str] = None,
89
90
  project_id: Optional[str] = None,
91
+ stop_reason: Optional[LettaStopReason] = None,
92
+ status: Optional[StepStatus] = None,
93
+ error_type: Optional[str] = None,
94
+ error_data: Optional[Dict] = None,
90
95
  ) -> PydanticStep:
91
96
  step_data = {
92
97
  "origin": None,
@@ -106,9 +111,14 @@ class StepManager:
106
111
  "tid": None,
107
112
  "trace_id": get_trace_id(), # Get the current trace ID
108
113
  "project_id": project_id,
114
+ "status": status if status else StepStatus.PENDING,
115
+ "error_type": error_type,
116
+ "error_data": error_data,
109
117
  }
110
118
  if step_id:
111
119
  step_data["id"] = step_id
120
+ if stop_reason:
121
+ step_data["stop_reason"] = stop_reason.stop_reason
112
122
  with db_registry.session() as session:
113
123
  if job_id:
114
124
  self._verify_job_access(session, job_id, actor, access=["write"])
@@ -133,6 +143,9 @@ class StepManager:
133
143
  step_id: Optional[str] = None,
134
144
  project_id: Optional[str] = None,
135
145
  stop_reason: Optional[LettaStopReason] = None,
146
+ status: Optional[StepStatus] = None,
147
+ error_type: Optional[str] = None,
148
+ error_data: Optional[Dict] = None,
136
149
  ) -> PydanticStep:
137
150
  step_data = {
138
151
  "origin": None,
@@ -152,6 +165,9 @@ class StepManager:
152
165
  "tid": None,
153
166
  "trace_id": get_trace_id(), # Get the current trace ID
154
167
  "project_id": project_id,
168
+ "status": status if status else StepStatus.PENDING,
169
+ "error_type": error_type,
170
+ "error_data": error_data,
155
171
  }
156
172
  if step_id:
157
173
  step_data["id"] = step_id
@@ -236,6 +252,126 @@ class StepManager:
236
252
  await session.commit()
237
253
  return step
238
254
 
255
+ @enforce_types
256
+ @trace_method
257
+ async def update_step_error_async(
258
+ self,
259
+ actor: PydanticUser,
260
+ step_id: str,
261
+ error_type: str,
262
+ error_message: str,
263
+ error_traceback: str,
264
+ error_details: Optional[Dict] = None,
265
+ stop_reason: Optional[LettaStopReason] = None,
266
+ ) -> PydanticStep:
267
+ """Update a step with error information.
268
+
269
+ Args:
270
+ actor: The user making the request
271
+ step_id: The ID of the step to update
272
+ error_type: The type/class of the error
273
+ error_message: The error message
274
+ error_traceback: Full error traceback
275
+ error_details: Additional error context
276
+ stop_reason: The stop reason to set
277
+
278
+ Returns:
279
+ The updated step
280
+
281
+ Raises:
282
+ NoResultFound: If the step does not exist
283
+ """
284
+ async with db_registry.async_session() as session:
285
+ step = await session.get(StepModel, step_id)
286
+ if not step:
287
+ raise NoResultFound(f"Step with id {step_id} does not exist")
288
+ if step.organization_id != actor.organization_id:
289
+ raise Exception("Unauthorized")
290
+
291
+ step.status = StepStatus.FAILED
292
+ step.error_type = error_type
293
+ step.error_data = {"message": error_message, "traceback": error_traceback, "details": error_details}
294
+ if stop_reason:
295
+ step.stop_reason = stop_reason.stop_reason
296
+
297
+ await session.commit()
298
+ return step.to_pydantic()
299
+
300
+ @enforce_types
301
+ @trace_method
302
+ async def update_step_success_async(
303
+ self,
304
+ actor: PydanticUser,
305
+ step_id: str,
306
+ usage: UsageStatistics,
307
+ stop_reason: Optional[LettaStopReason] = None,
308
+ ) -> PydanticStep:
309
+ """Update a step with success status and final usage statistics.
310
+
311
+ Args:
312
+ actor: The user making the request
313
+ step_id: The ID of the step to update
314
+ usage: Final usage statistics
315
+ stop_reason: The stop reason to set
316
+
317
+ Returns:
318
+ The updated step
319
+
320
+ Raises:
321
+ NoResultFound: If the step does not exist
322
+ """
323
+ async with db_registry.async_session() as session:
324
+ step = await session.get(StepModel, step_id)
325
+ if not step:
326
+ raise NoResultFound(f"Step with id {step_id} does not exist")
327
+ if step.organization_id != actor.organization_id:
328
+ raise Exception("Unauthorized")
329
+
330
+ step.status = StepStatus.SUCCESS
331
+ step.completion_tokens = usage.completion_tokens
332
+ step.prompt_tokens = usage.prompt_tokens
333
+ step.total_tokens = usage.total_tokens
334
+ if stop_reason:
335
+ step.stop_reason = stop_reason.stop_reason
336
+
337
+ await session.commit()
338
+ return step.to_pydantic()
339
+
340
+ @enforce_types
341
+ @trace_method
342
+ async def update_step_cancelled_async(
343
+ self,
344
+ actor: PydanticUser,
345
+ step_id: str,
346
+ stop_reason: Optional[LettaStopReason] = None,
347
+ ) -> PydanticStep:
348
+ """Update a step with cancelled status.
349
+
350
+ Args:
351
+ actor: The user making the request
352
+ step_id: The ID of the step to update
353
+ stop_reason: The stop reason to set
354
+
355
+ Returns:
356
+ The updated step
357
+
358
+ Raises:
359
+ NoResultFound: If the step does not exist
360
+ """
361
+ async with db_registry.async_session() as session:
362
+ step = await session.get(StepModel, step_id)
363
+ if not step:
364
+ raise NoResultFound(f"Step with id {step_id} does not exist")
365
+ if step.organization_id != actor.organization_id:
366
+ raise Exception("Unauthorized")
367
+
368
+ step.status = StepStatus.CANCELLED
369
+ if stop_reason:
370
+ step.stop_reason = stop_reason.stop_reason
371
+
372
+ await session.commit()
373
+ return step.to_pydantic()
374
+
239
375
  def _verify_job_access(
240
376
  self,
241
377
  session: Session,
@@ -319,6 +455,10 @@ class NoopStepManager(StepManager):
319
455
  job_id: Optional[str] = None,
320
456
  step_id: Optional[str] = None,
321
457
  project_id: Optional[str] = None,
458
+ stop_reason: Optional[LettaStopReason] = None,
459
+ status: Optional[StepStatus] = None,
460
+ error_type: Optional[str] = None,
461
+ error_data: Optional[Dict] = None,
322
462
  ) -> PydanticStep:
323
463
  return
324
464
 
@@ -339,5 +479,43 @@ class NoopStepManager(StepManager):
339
479
  step_id: Optional[str] = None,
340
480
  project_id: Optional[str] = None,
341
481
  stop_reason: Optional[LettaStopReason] = None,
482
+ status: Optional[StepStatus] = None,
483
+ error_type: Optional[str] = None,
484
+ error_data: Optional[Dict] = None,
485
+ ) -> PydanticStep:
486
+ return
487
+
488
+ @enforce_types
489
+ @trace_method
490
+ async def update_step_error_async(
491
+ self,
492
+ actor: PydanticUser,
493
+ step_id: str,
494
+ error_type: str,
495
+ error_message: str,
496
+ error_traceback: str,
497
+ error_details: Optional[Dict] = None,
498
+ stop_reason: Optional[LettaStopReason] = None,
499
+ ) -> PydanticStep:
500
+ return
501
+
502
+ @enforce_types
503
+ @trace_method
504
+ async def update_step_success_async(
505
+ self,
506
+ actor: PydanticUser,
507
+ step_id: str,
508
+ usage: UsageStatistics,
509
+ stop_reason: Optional[LettaStopReason] = None,
510
+ ) -> PydanticStep:
511
+ return
512
+
513
+ @enforce_types
514
+ @trace_method
515
+ async def update_step_cancelled_async(
516
+ self,
517
+ actor: PydanticUser,
518
+ step_id: str,
519
+ stop_reason: Optional[LettaStopReason] = None,
342
520
  ) -> PydanticStep:
343
521
  return
@@ -210,7 +210,10 @@ class LettaBuiltinToolExecutor(ToolExecutor):
210
210
  logger.info(f"[DEBUG] Starting Firecrawl search for query: '{task.query}' with limit={limit}")
211
211
 
212
212
  # Perform the search for this task
213
- search_result = await app.search(task.query, limit=limit, scrape_options=ScrapeOptions(formats=["markdown"]))
213
+ scrape_options = ScrapeOptions(
214
+ formats=["markdown"], excludeTags=["#ad", "#footer"], onlyMainContent=True, parsePDF=True, removeBase64Images=True
215
+ )
216
+ search_result = await app.search(task.query, limit=limit, scrape_options=scrape_options)
214
217
 
215
218
  logger.info(
216
219
  f"[DEBUG] Firecrawl search completed for '{task.query}': {len(search_result.get('data', [])) if search_result else 0} results"
@@ -230,14 +230,7 @@ class LettaCoreToolExecutor(ToolExecutor):
230
230
  await AgentManager().update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
231
231
  return None
232
232
 
233
- async def memory_replace(
234
- self,
235
- agent_state: AgentState,
236
- actor: User,
237
- label: str,
238
- old_str: str,
239
- new_str: Optional[str] = None,
240
- ) -> str:
233
+ async def memory_replace(self, agent_state: AgentState, actor: User, label: str, old_str: str, new_str: str) -> str:
241
234
  """
242
235
  The memory_replace command allows you to replace a specific string in a memory
243
236
  block with a new string. This is used for making precise edits.
@@ -246,8 +239,7 @@ class LettaCoreToolExecutor(ToolExecutor):
246
239
  label (str): Section of the memory to be edited, identified by its label.
247
240
  old_str (str): The text to replace (must match exactly, including whitespace
248
241
  and indentation). Do not include line number prefixes.
249
- new_str (Optional[str]): The new text to insert in place of the old text.
250
- Omit this argument to delete the old_str. Do not include line number prefixes.
242
+ new_str (str): The new text to insert in place of the old text. Do not include line number prefixes.
251
243
 
252
244
  Returns:
253
245
  str: The success message
@@ -32,10 +32,12 @@ class LettaFileToolExecutor(ToolExecutor):
32
32
  MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024 # 50MB limit per file
33
33
  MAX_TOTAL_CONTENT_SIZE = 200 * 1024 * 1024 # 200MB total across all files
34
34
  MAX_REGEX_COMPLEXITY = 1000 # Prevent catastrophic backtracking
35
- MAX_MATCHES_PER_FILE = 20 # Limit matches per file
36
- MAX_TOTAL_MATCHES = 50 # Global match limit
35
+ MAX_MATCHES_PER_FILE = 20 # Limit matches per file (legacy, not used with new pagination)
36
+ MAX_TOTAL_MATCHES = 50 # Keep original value for semantic search
37
+ GREP_PAGE_SIZE = 20 # Number of grep matches to show per page
37
38
  GREP_TIMEOUT_SECONDS = 30 # Max time for grep_files operation
38
39
  MAX_CONTEXT_LINES = 1 # Lines of context around matches
40
+ MAX_TOTAL_COLLECTED = 1000 # Reasonable upper limit to prevent memory issues
39
41
 
40
42
  def __init__(
41
43
  self,
@@ -298,7 +300,12 @@ class LettaFileToolExecutor(ToolExecutor):
298
300
 
299
301
  @trace_method
300
302
  async def grep_files(
301
- self, agent_state: AgentState, pattern: str, include: Optional[str] = None, context_lines: Optional[int] = 3
303
+ self,
304
+ agent_state: AgentState,
305
+ pattern: str,
306
+ include: Optional[str] = None,
307
+ context_lines: Optional[int] = 1,
308
+ offset: Optional[int] = None,
302
309
  ) -> str:
303
310
  """
304
311
  Search for pattern in all attached files and return matches with context.
@@ -308,7 +315,9 @@ class LettaFileToolExecutor(ToolExecutor):
308
315
  pattern: Regular expression pattern to search for
309
316
  include: Optional pattern to filter filenames to include in the search
310
317
  context_lines (Optional[int]): Number of lines of context to show before and after each match.
311
- Equivalent to `-C` in grep_files. Defaults to 3.
318
+ Equivalent to `-C` in grep_files. Defaults to 1.
319
+ offset (Optional[int]): Number of matches to skip before showing results. Used for pagination.
320
+ Defaults to 0 (show from first match).
312
321
 
313
322
  Returns:
314
323
  Formatted string with search results, file names, line numbers, and context
@@ -350,14 +359,18 @@ class LettaFileToolExecutor(ToolExecutor):
350
359
  if not file_agents:
351
360
  return f"No files match the filename pattern '{include}' (filtered {original_count} files)"
352
361
 
362
+ # Validate offset parameter
363
+ if offset is not None and offset < 0:
364
+ offset = 0 # Treat negative offsets as 0
365
+
353
366
  # Compile regex pattern with appropriate flags
354
367
  regex_flags = re.MULTILINE
355
368
  regex_flags |= re.IGNORECASE
356
369
 
357
370
  pattern_regex = re.compile(pattern, regex_flags)
358
371
 
359
- results = []
360
- total_matches = 0
372
+ # Collect all matches first (up to a reasonable limit)
373
+ all_matches = [] # List of tuples: (file_name, line_num, context_lines)
361
374
  total_content_size = 0
362
375
  files_processed = 0
363
376
  files_skipped = 0
@@ -365,7 +378,7 @@ class LettaFileToolExecutor(ToolExecutor):
365
378
 
366
379
  # Use asyncio timeout to prevent hanging
367
380
  async def _search_files():
368
- nonlocal results, total_matches, total_content_size, files_processed, files_skipped, files_with_matches
381
+ nonlocal all_matches, total_content_size, files_processed, files_skipped, files_with_matches
369
382
 
370
383
  for file_agent in file_agents:
371
384
  # Load file content
@@ -383,7 +396,6 @@ class LettaFileToolExecutor(ToolExecutor):
383
396
  self.logger.warning(
384
397
  f"Grep: Skipping file {file.file_name} - too large ({content_size:,} bytes > {self.MAX_FILE_SIZE_BYTES:,} limit)"
385
398
  )
386
- results.append(f"[SKIPPED] {file.file_name}: File too large ({content_size:,} bytes)")
387
399
  continue
388
400
 
389
401
  # Check total content size across all files
@@ -393,11 +405,9 @@ class LettaFileToolExecutor(ToolExecutor):
393
405
  self.logger.warning(
394
406
  f"Grep: Skipping file {file.file_name} - total content size limit exceeded ({total_content_size:,} bytes > {self.MAX_TOTAL_CONTENT_SIZE:,} limit)"
395
407
  )
396
- results.append(f"[SKIPPED] {file.file_name}: Total content size limit exceeded")
397
408
  break
398
409
 
399
410
  files_processed += 1
400
- file_matches = 0
401
411
 
402
412
  # Use LineChunker to get all lines with proper formatting
403
413
  chunker = LineChunker()
@@ -407,16 +417,10 @@ class LettaFileToolExecutor(ToolExecutor):
407
417
  if formatted_lines and formatted_lines[0].startswith("[Viewing"):
408
418
  formatted_lines = formatted_lines[1:]
409
419
 
410
- # LineChunker now returns 1-indexed line numbers, so no conversion needed
411
-
412
420
  # Search for matches in formatted lines
413
421
  for formatted_line in formatted_lines:
414
- if total_matches >= self.MAX_TOTAL_MATCHES:
415
- results.append(f"[TRUNCATED] Maximum total matches ({self.MAX_TOTAL_MATCHES}) reached")
416
- return
417
-
418
- if file_matches >= self.MAX_MATCHES_PER_FILE:
419
- results.append(f"[TRUNCATED] {file.file_name}: Maximum matches per file ({self.MAX_MATCHES_PER_FILE}) reached")
422
+ if len(all_matches) >= self.MAX_TOTAL_COLLECTED:
423
+ # Stop collecting if we hit the upper limit
420
424
  break
421
425
 
422
426
  # Extract line number and content from formatted line
@@ -433,16 +437,11 @@ class LettaFileToolExecutor(ToolExecutor):
433
437
  files_with_matches.add(file.file_name)
434
438
  context = self._get_context_lines(formatted_lines, match_line_num=line_num, context_lines=context_lines or 0)
435
439
 
436
- # Format the match result
437
- match_header = f"\n=== {file.file_name}:{line_num} ==="
438
- match_content = "\n".join(context)
439
- results.append(f"{match_header}\n{match_content}")
440
-
441
- file_matches += 1
442
- total_matches += 1
440
+ # Store match data for later pagination
441
+ all_matches.append((file.file_name, line_num, context))
443
442
 
444
- # Break if global limits reached
445
- if total_matches >= self.MAX_TOTAL_MATCHES:
443
+ # Break if we've collected enough matches
444
+ if len(all_matches) >= self.MAX_TOTAL_COLLECTED:
446
445
  break
447
446
 
448
447
  # Execute with timeout
@@ -452,8 +451,9 @@ class LettaFileToolExecutor(ToolExecutor):
452
451
  if files_with_matches:
453
452
  await self.files_agents_manager.mark_access_bulk(agent_id=agent_state.id, file_names=list(files_with_matches), actor=self.actor)
454
453
 
455
- # Format final results
456
- if not results or total_matches == 0:
454
+ # Handle no matches case
455
+ total_matches = len(all_matches)
456
+ if total_matches == 0:
457
457
  summary = f"No matches found for pattern: '{pattern}'"
458
458
  if include:
459
459
  summary += f" in files matching '{include}'"
@@ -461,21 +461,70 @@ class LettaFileToolExecutor(ToolExecutor):
461
461
  summary += f" (searched {files_processed} files, skipped {files_skipped})"
462
462
  return summary
463
463
 
464
- # Add summary header
465
- summary_parts = [f"Found {total_matches} matches"]
466
- if files_processed > 0:
467
- summary_parts.append(f"in {files_processed} files")
464
+ # Apply pagination
465
+ start_idx = offset if offset else 0
466
+ end_idx = start_idx + self.GREP_PAGE_SIZE
467
+ paginated_matches = all_matches[start_idx:end_idx]
468
+
469
+ # Check if we hit the collection limit
470
+ hit_collection_limit = len(all_matches) >= self.MAX_TOTAL_COLLECTED
471
+
472
+ # Format the paginated results
473
+ results = []
474
+
475
+ # Build summary showing the range of matches displayed
476
+ if hit_collection_limit:
477
+ # We collected MAX_TOTAL_COLLECTED but there might be more
478
+ summary = f"Found {self.MAX_TOTAL_COLLECTED}+ total matches across {len(files_with_matches)} files (showing matches {start_idx + 1}-{min(end_idx, total_matches)} of {self.MAX_TOTAL_COLLECTED}+)"
479
+ else:
480
+ # We found all matches
481
+ summary = f"Found {total_matches} total matches across {len(files_with_matches)} files (showing matches {start_idx + 1}-{min(end_idx, total_matches)} of {total_matches})"
482
+
468
483
  if files_skipped > 0:
469
- summary_parts.append(f"({files_skipped} files skipped)")
484
+ summary += f"\nNote: Skipped {files_skipped} files due to size limits"
470
485
 
471
- summary = " ".join(summary_parts) + f" for pattern: '{pattern}'"
472
- if include:
473
- summary += f" in files matching '{include}'"
486
+ results.append(summary)
487
+ results.append("=" * 80)
474
488
 
475
- # Combine all results
476
- formatted_results = [summary, "=" * len(summary)] + results
489
+ # Add file summary - count matches per file
490
+ file_match_counts = {}
491
+ for file_name, _, _ in all_matches:
492
+ file_match_counts[file_name] = file_match_counts.get(file_name, 0) + 1
477
493
 
478
- return "\n".join(formatted_results)
494
+ # Sort files by match count (descending) for better overview
495
+ sorted_files = sorted(file_match_counts.items(), key=lambda x: x[1], reverse=True)
496
+
497
+ results.append("\nFiles with matches:")
498
+ for file_name, count in sorted_files:
499
+ if hit_collection_limit and count >= self.MAX_TOTAL_COLLECTED:
500
+ results.append(f" - {file_name}: {count}+ matches")
501
+ else:
502
+ results.append(f" - {file_name}: {count} matches")
503
+ results.append("") # blank line before matches
504
+
505
+ # Format each match in the current page
506
+ for file_name, line_num, context_lines in paginated_matches:
507
+ match_header = f"\n=== {file_name}:{line_num} ==="
508
+ match_content = "\n".join(context_lines)
509
+ results.append(f"{match_header}\n{match_content}")
510
+
511
+ # Add navigation hint
512
+ results.append("") # blank line
513
+ if end_idx < total_matches:
514
+ if hit_collection_limit:
515
+ results.append(f'To see more matches, call: grep_files(pattern="{pattern}", offset={end_idx})')
516
+ results.append(
517
+ f"Note: Only the first {self.MAX_TOTAL_COLLECTED} matches were collected. There may be more matches beyond this limit."
518
+ )
519
+ else:
520
+ results.append(f'To see more matches, call: grep_files(pattern="{pattern}", offset={end_idx})')
521
+ else:
522
+ if hit_collection_limit:
523
+ results.append("Showing last page of collected matches. There may be more matches beyond the collection limit.")
524
+ else:
525
+ results.append("No more matches to show.")
526
+
527
+ return "\n".join(results)
479
528
 
480
529
  @trace_method
481
530
  async def semantic_search_files(self, agent_state: AgentState, query: str, limit: int = 5) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.11.0.dev20250807104511
3
+ Version: 0.11.0.dev20250808104456
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team