letta-nightly 0.11.7.dev20250908104137__py3-none-any.whl → 0.11.7.dev20250910104051__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 (70) hide show
  1. letta/adapters/letta_llm_adapter.py +81 -0
  2. letta/adapters/letta_llm_request_adapter.py +111 -0
  3. letta/adapters/letta_llm_stream_adapter.py +169 -0
  4. letta/agents/base_agent.py +4 -1
  5. letta/agents/base_agent_v2.py +68 -0
  6. letta/agents/helpers.py +3 -5
  7. letta/agents/letta_agent.py +23 -12
  8. letta/agents/letta_agent_v2.py +1220 -0
  9. letta/agents/voice_agent.py +2 -1
  10. letta/constants.py +1 -1
  11. letta/errors.py +12 -0
  12. letta/functions/function_sets/base.py +53 -12
  13. letta/functions/schema_generator.py +1 -1
  14. letta/groups/sleeptime_multi_agent_v3.py +231 -0
  15. letta/helpers/tool_rule_solver.py +4 -0
  16. letta/helpers/tpuf_client.py +607 -34
  17. letta/interfaces/anthropic_streaming_interface.py +64 -24
  18. letta/interfaces/openai_streaming_interface.py +80 -37
  19. letta/llm_api/openai_client.py +45 -4
  20. letta/orm/block.py +1 -0
  21. letta/orm/group.py +1 -0
  22. letta/orm/source.py +8 -1
  23. letta/orm/step_metrics.py +10 -0
  24. letta/schemas/block.py +4 -0
  25. letta/schemas/enums.py +1 -0
  26. letta/schemas/group.py +8 -0
  27. letta/schemas/letta_message.py +1 -1
  28. letta/schemas/letta_request.py +2 -2
  29. letta/schemas/mcp.py +9 -1
  30. letta/schemas/message.py +23 -0
  31. letta/schemas/providers/ollama.py +1 -1
  32. letta/schemas/providers.py +1 -2
  33. letta/schemas/source.py +6 -0
  34. letta/schemas/step_metrics.py +2 -0
  35. letta/server/rest_api/routers/v1/__init__.py +2 -0
  36. letta/server/rest_api/routers/v1/agents.py +100 -5
  37. letta/server/rest_api/routers/v1/blocks.py +6 -0
  38. letta/server/rest_api/routers/v1/folders.py +23 -5
  39. letta/server/rest_api/routers/v1/groups.py +6 -0
  40. letta/server/rest_api/routers/v1/internal_templates.py +218 -12
  41. letta/server/rest_api/routers/v1/messages.py +14 -19
  42. letta/server/rest_api/routers/v1/runs.py +43 -28
  43. letta/server/rest_api/routers/v1/sources.py +23 -5
  44. letta/server/rest_api/routers/v1/tools.py +42 -0
  45. letta/server/rest_api/streaming_response.py +9 -1
  46. letta/server/server.py +2 -1
  47. letta/services/agent_manager.py +39 -59
  48. letta/services/agent_serialization_manager.py +22 -8
  49. letta/services/archive_manager.py +60 -9
  50. letta/services/block_manager.py +5 -0
  51. letta/services/file_processor/embedder/base_embedder.py +5 -0
  52. letta/services/file_processor/embedder/openai_embedder.py +4 -0
  53. letta/services/file_processor/embedder/pinecone_embedder.py +5 -1
  54. letta/services/file_processor/embedder/turbopuffer_embedder.py +71 -0
  55. letta/services/file_processor/file_processor.py +9 -7
  56. letta/services/group_manager.py +74 -11
  57. letta/services/mcp_manager.py +132 -26
  58. letta/services/message_manager.py +229 -125
  59. letta/services/passage_manager.py +2 -1
  60. letta/services/source_manager.py +23 -1
  61. letta/services/summarizer/summarizer.py +2 -0
  62. letta/services/tool_executor/core_tool_executor.py +2 -120
  63. letta/services/tool_executor/files_tool_executor.py +133 -8
  64. letta/settings.py +6 -0
  65. letta/utils.py +34 -1
  66. {letta_nightly-0.11.7.dev20250908104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/METADATA +2 -2
  67. {letta_nightly-0.11.7.dev20250908104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/RECORD +70 -63
  68. {letta_nightly-0.11.7.dev20250908104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/WHEEL +0 -0
  69. {letta_nightly-0.11.7.dev20250908104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/entry_points.txt +0 -0
  70. {letta_nightly-0.11.7.dev20250908104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/licenses/LICENSE +0 -0
@@ -71,15 +71,6 @@ class LettaCoreToolExecutor(ToolExecutor):
71
71
  )
72
72
 
73
73
  async def send_message(self, agent_state: AgentState, actor: User, message: str) -> Optional[str]:
74
- """
75
- Sends a message to the human user.
76
-
77
- Args:
78
- message (str): Message contents. All unicode (including emojis) are supported.
79
-
80
- Returns:
81
- Optional[str]: None is always returned as this function does not produce a response.
82
- """
83
74
  return "Sent message successfully."
84
75
 
85
76
  async def conversation_search(
@@ -92,19 +83,6 @@ class LettaCoreToolExecutor(ToolExecutor):
92
83
  start_date: Optional[str] = None,
93
84
  end_date: Optional[str] = None,
94
85
  ) -> Optional[str]:
95
- """
96
- Search prior conversation history using hybrid search (text + semantic similarity).
97
-
98
- Args:
99
- query (str): String to search for using both text matching and semantic similarity.
100
- roles (Optional[List[Literal["assistant", "user", "tool"]]]): Optional list of message roles to filter by.
101
- limit (Optional[int]): Maximum number of results to return. Uses system default if not specified.
102
- start_date (Optional[str]): Filter results to messages created after this date. ISO 8601 format: "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM". Examples: "2024-01-15", "2024-01-15T14:30".
103
- end_date (Optional[str]): Filter results to messages created before this date. ISO 8601 format: "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM". Examples: "2024-01-20", "2024-01-20T17:00".
104
-
105
- Returns:
106
- str: Query result string containing matching messages with timestamps and content.
107
- """
108
86
  try:
109
87
  # Parse datetime parameters if provided
110
88
  start_datetime = None
@@ -163,7 +141,6 @@ class LettaCoreToolExecutor(ToolExecutor):
163
141
  limit=search_limit,
164
142
  start_date=start_datetime,
165
143
  end_date=end_datetime,
166
- embedding_config=agent_state.embedding_config,
167
144
  )
168
145
 
169
146
  if len(message_results) == 0:
@@ -286,23 +263,9 @@ class LettaCoreToolExecutor(ToolExecutor):
286
263
  start_datetime: Optional[str] = None,
287
264
  end_datetime: Optional[str] = None,
288
265
  ) -> Optional[str]:
289
- """
290
- Search archival memory using semantic (embedding-based) search with optional temporal filtering.
291
-
292
- Args:
293
- query (str): String to search for using semantic similarity.
294
- tags (Optional[list[str]]): Optional list of tags to filter search results. Only passages with these tags will be returned.
295
- tag_match_mode (Literal["any", "all"]): How to match tags - "any" to match passages with any of the tags, "all" to match only passages with all tags. Defaults to "any".
296
- top_k (Optional[int]): Maximum number of results to return. Uses system default if not specified.
297
- start_datetime (Optional[str]): Filter results to passages created after this datetime. ISO 8601 format.
298
- end_datetime (Optional[str]): Filter results to passages created before this datetime. ISO 8601 format.
299
-
300
- Returns:
301
- str: Query result string containing matching passages with timestamps, content, and tags.
302
- """
303
266
  try:
304
267
  # Use the shared service method to get results
305
- formatted_results, count = await self.agent_manager.search_agent_archival_memory_async(
268
+ formatted_results = await self.agent_manager.search_agent_archival_memory_async(
306
269
  agent_id=agent_state.id,
307
270
  actor=actor,
308
271
  query=query,
@@ -313,7 +276,7 @@ class LettaCoreToolExecutor(ToolExecutor):
313
276
  end_datetime=end_datetime,
314
277
  )
315
278
 
316
- return formatted_results, count
279
+ return formatted_results
317
280
 
318
281
  except Exception as e:
319
282
  raise e
@@ -321,16 +284,6 @@ class LettaCoreToolExecutor(ToolExecutor):
321
284
  async def archival_memory_insert(
322
285
  self, agent_state: AgentState, actor: User, content: str, tags: Optional[list[str]] = None
323
286
  ) -> Optional[str]:
324
- """
325
- Add to archival memory. Make sure to phrase the memory contents such that it can be easily queried later.
326
-
327
- Args:
328
- content (str): Content to write to the memory. All unicode (including emojis) are supported.
329
- tags (Optional[list[str]]): Optional list of tags to associate with this memory for better organization and filtering.
330
-
331
- Returns:
332
- Optional[str]: None is always returned as this function does not produce a response.
333
- """
334
287
  await self.passage_manager.insert_passage(
335
288
  agent_state=agent_state,
336
289
  text=content,
@@ -341,16 +294,6 @@ class LettaCoreToolExecutor(ToolExecutor):
341
294
  return None
342
295
 
343
296
  async def core_memory_append(self, agent_state: AgentState, actor: User, label: str, content: str) -> Optional[str]:
344
- """
345
- Append to the contents of core memory.
346
-
347
- Args:
348
- label (str): Section of the memory to be edited.
349
- content (str): Content to write to the memory. All unicode (including emojis) are supported.
350
-
351
- Returns:
352
- Optional[str]: None is always returned as this function does not produce a response.
353
- """
354
297
  if agent_state.memory.get_block(label).read_only:
355
298
  raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
356
299
  current_value = str(agent_state.memory.get_block(label).value)
@@ -367,17 +310,6 @@ class LettaCoreToolExecutor(ToolExecutor):
367
310
  old_content: str,
368
311
  new_content: str,
369
312
  ) -> Optional[str]:
370
- """
371
- Replace the contents of core memory. To delete memories, use an empty string for new_content.
372
-
373
- Args:
374
- label (str): Section of the memory to be edited.
375
- old_content (str): String to replace. Must be an exact match.
376
- new_content (str): Content to write to the memory. All unicode (including emojis) are supported.
377
-
378
- Returns:
379
- Optional[str]: None is always returned as this function does not produce a response.
380
- """
381
313
  if agent_state.memory.get_block(label).read_only:
382
314
  raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
383
315
  current_value = str(agent_state.memory.get_block(label).value)
@@ -389,20 +321,6 @@ class LettaCoreToolExecutor(ToolExecutor):
389
321
  return None
390
322
 
391
323
  async def memory_replace(self, agent_state: AgentState, actor: User, label: str, old_str: str, new_str: str) -> str:
392
- """
393
- The memory_replace command allows you to replace a specific string in a memory
394
- block with a new string. This is used for making precise edits.
395
-
396
- Args:
397
- label (str): Section of the memory to be edited, identified by its label.
398
- old_str (str): The text to replace (must match exactly, including whitespace
399
- and indentation). Do not include line number prefixes.
400
- new_str (str): The new text to insert in place of the old text. Do not include line number prefixes.
401
-
402
- Returns:
403
- str: The success message
404
- """
405
-
406
324
  if agent_state.memory.get_block(label).read_only:
407
325
  raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
408
326
 
@@ -479,20 +397,6 @@ class LettaCoreToolExecutor(ToolExecutor):
479
397
  new_str: str,
480
398
  insert_line: int = -1,
481
399
  ) -> str:
482
- """
483
- The memory_insert command allows you to insert text at a specific location
484
- in a memory block.
485
-
486
- Args:
487
- label (str): Section of the memory to be edited, identified by its label.
488
- new_str (str): The text to insert. Do not include line number prefixes.
489
- insert_line (int): The line number after which to insert the text (0 for
490
- beginning of file). Defaults to -1 (end of the file).
491
-
492
- Returns:
493
- str: The success message
494
- """
495
-
496
400
  if agent_state.memory.get_block(label).read_only:
497
401
  raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
498
402
 
@@ -559,20 +463,6 @@ class LettaCoreToolExecutor(ToolExecutor):
559
463
  return success_msg
560
464
 
561
465
  async def memory_rethink(self, agent_state: AgentState, actor: User, label: str, new_memory: str) -> str:
562
- """
563
- The memory_rethink command allows you to completely rewrite the contents of a
564
- memory block. Use this tool to make large sweeping changes (e.g. when you want
565
- to condense or reorganize the memory blocks), do NOT use this tool to make small
566
- precise edits (e.g. add or remove a line, replace a specific string, etc).
567
-
568
- Args:
569
- label (str): The memory block to be rewritten, identified by its label.
570
- new_memory (str): The new memory contents with information integrated from
571
- existing memory blocks and the conversation context. Do not include line number prefixes.
572
-
573
- Returns:
574
- str: The success message
575
- """
576
466
  if agent_state.memory.get_block(label).read_only:
577
467
  raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
578
468
 
@@ -611,12 +501,4 @@ class LettaCoreToolExecutor(ToolExecutor):
611
501
  return success_msg
612
502
 
613
503
  async def memory_finish_edits(self, agent_state: AgentState, actor: User) -> None:
614
- """
615
- Call the memory_finish_edits command when you are finished making edits
616
- (integrating all new information) into the memory blocks. This function
617
- is called when the agent is done rethinking the memory.
618
-
619
- Returns:
620
- Optional[str]: None is always returned as this function does not produce a response.
621
- """
622
504
  return None
@@ -5,10 +5,13 @@ from typing import Any, Dict, List, Optional
5
5
  from letta.constants import PINECONE_TEXT_FIELD_NAME
6
6
  from letta.functions.types import FileOpenRequest
7
7
  from letta.helpers.pinecone_utils import search_pinecone_index, should_use_pinecone
8
+ from letta.helpers.tpuf_client import should_use_tpuf
8
9
  from letta.log import get_logger
9
10
  from letta.otel.tracing import trace_method
10
11
  from letta.schemas.agent import AgentState
12
+ from letta.schemas.enums import VectorDBProvider
11
13
  from letta.schemas.sandbox_config import SandboxConfig
14
+ from letta.schemas.source import Source
12
15
  from letta.schemas.tool import Tool
13
16
  from letta.schemas.tool_execution_result import ToolExecutionResult
14
17
  from letta.schemas.user import User
@@ -554,18 +557,140 @@ class LettaFileToolExecutor(ToolExecutor):
554
557
 
555
558
  self.logger.info(f"Semantic search started for agent {agent_state.id} with query '{query}' (limit: {limit})")
556
559
 
557
- # Check if Pinecone is enabled and use it if available
558
- if should_use_pinecone():
559
- return await self._search_files_pinecone(agent_state, query, limit)
560
- else:
561
- return await self._search_files_traditional(agent_state, query, limit)
560
+ # Check which vector DB to use - Turbopuffer takes precedence
561
+ attached_sources = await self.agent_manager.list_attached_sources_async(agent_id=agent_state.id, actor=self.actor)
562
+ attached_tpuf_sources = [source for source in attached_sources if source.vector_db_provider == VectorDBProvider.TPUF]
563
+ attached_pinecone_sources = [source for source in attached_sources if source.vector_db_provider == VectorDBProvider.PINECONE]
564
+
565
+ if not attached_tpuf_sources and not attached_pinecone_sources:
566
+ return await self._search_files_native(agent_state, query, limit)
567
+
568
+ results = []
569
+
570
+ # If both have items, we half the limit roughly
571
+ # TODO: This is very hacky bc it skips the re-ranking - but this is a temporary stopgap while we think about migrating data
572
+
573
+ if attached_tpuf_sources and attached_pinecone_sources:
574
+ limit = max(limit // 2, 1)
575
+
576
+ if should_use_tpuf() and attached_tpuf_sources:
577
+ tpuf_result = await self._search_files_turbopuffer(agent_state, attached_tpuf_sources, query, limit)
578
+ results.append(tpuf_result)
579
+
580
+ if should_use_pinecone() and attached_pinecone_sources:
581
+ pinecone_result = await self._search_files_pinecone(agent_state, attached_pinecone_sources, query, limit)
582
+ results.append(pinecone_result)
583
+
584
+ # combine results from both sources
585
+ if results:
586
+ return "\n\n".join(results)
587
+
588
+ # fallback if no results from either source
589
+ return "No results found"
590
+
591
+ async def _search_files_turbopuffer(self, agent_state: AgentState, attached_sources: List[Source], query: str, limit: int) -> str:
592
+ """Search files using Turbopuffer vector database."""
593
+
594
+ # Get attached sources
595
+ source_ids = [source.id for source in attached_sources]
596
+ if not source_ids:
597
+ return "No valid source IDs found for attached files"
598
+
599
+ # Get all attached files for this agent
600
+ file_agents = await self.files_agents_manager.list_files_for_agent(
601
+ agent_id=agent_state.id, per_file_view_window_char_limit=agent_state.per_file_view_window_char_limit, actor=self.actor
602
+ )
603
+ if not file_agents:
604
+ return "No files are currently attached to search"
605
+
606
+ # Create a map of file_id to file_name for quick lookup
607
+ file_map = {fa.file_id: fa.file_name for fa in file_agents}
608
+
609
+ results = []
610
+ total_hits = 0
611
+ files_with_matches = {}
612
+
613
+ try:
614
+ from letta.helpers.tpuf_client import TurbopufferClient
615
+
616
+ tpuf_client = TurbopufferClient()
617
+
618
+ # Query Turbopuffer for all sources at once
619
+ search_results = await tpuf_client.query_file_passages(
620
+ source_ids=source_ids, # pass all source_ids as a list
621
+ organization_id=self.actor.organization_id,
622
+ actor=self.actor,
623
+ query_text=query,
624
+ search_mode="hybrid", # use hybrid search for best results
625
+ top_k=limit,
626
+ )
627
+
628
+ # Process search results
629
+ for passage, score, metadata in search_results:
630
+ if total_hits >= limit:
631
+ break
632
+
633
+ total_hits += 1
634
+
635
+ # get file name from our map
636
+ file_name = file_map.get(passage.file_id, "Unknown File")
562
637
 
563
- async def _search_files_pinecone(self, agent_state: AgentState, query: str, limit: int) -> str:
638
+ # group by file name
639
+ if file_name not in files_with_matches:
640
+ files_with_matches[file_name] = []
641
+ files_with_matches[file_name].append({"text": passage.text, "score": score, "passage_id": passage.id})
642
+
643
+ except Exception as e:
644
+ self.logger.error(f"Turbopuffer search failed: {str(e)}")
645
+ raise e
646
+
647
+ if not files_with_matches:
648
+ return f"No semantic matches found in Turbopuffer for query: '{query}'"
649
+
650
+ # Format results
651
+ passage_num = 0
652
+ for file_name, matches in files_with_matches.items():
653
+ for match in matches:
654
+ passage_num += 1
655
+
656
+ # format each passage with terminal-style header
657
+ score_display = f"(score: {match['score']:.3f})"
658
+ passage_header = f"\n=== {file_name} (passage #{passage_num}) {score_display} ==="
659
+
660
+ # format the passage text
661
+ passage_text = match["text"].strip()
662
+ lines = passage_text.splitlines()
663
+ formatted_lines = []
664
+ for line in lines[:20]: # limit to first 20 lines per passage
665
+ formatted_lines.append(f" {line}")
666
+
667
+ if len(lines) > 20:
668
+ formatted_lines.append(f" ... [truncated {len(lines) - 20} more lines]")
669
+
670
+ passage_content = "\n".join(formatted_lines)
671
+ results.append(f"{passage_header}\n{passage_content}")
672
+
673
+ # mark access for files that had matches
674
+ if files_with_matches:
675
+ matched_file_names = [name for name in files_with_matches.keys() if name != "Unknown File"]
676
+ if matched_file_names:
677
+ await self.files_agents_manager.mark_access_bulk(agent_id=agent_state.id, file_names=matched_file_names, actor=self.actor)
678
+
679
+ # create summary header
680
+ file_count = len(files_with_matches)
681
+ summary = f"Found {total_hits} Turbopuffer matches in {file_count} file{'s' if file_count != 1 else ''} for query: '{query}'"
682
+
683
+ # combine all results
684
+ formatted_results = [summary, "=" * len(summary)] + results
685
+
686
+ self.logger.info(f"Turbopuffer search completed: {total_hits} matches across {file_count} files")
687
+ return "\n".join(formatted_results)
688
+
689
+ async def _search_files_pinecone(self, agent_state: AgentState, attached_sources: List[Source], query: str, limit: int) -> str:
564
690
  """Search files using Pinecone vector database."""
565
691
 
566
692
  # Extract unique source_ids
567
693
  # TODO: Inefficient
568
- attached_sources = await self.agent_manager.list_attached_sources_async(agent_id=agent_state.id, actor=self.actor)
569
694
  source_ids = [source.id for source in attached_sources]
570
695
  if not source_ids:
571
696
  return "No valid source IDs found for attached files"
@@ -658,7 +783,7 @@ class LettaFileToolExecutor(ToolExecutor):
658
783
  self.logger.info(f"Pinecone search completed: {total_hits} matches across {file_count} files")
659
784
  return "\n".join(formatted_results)
660
785
 
661
- async def _search_files_traditional(self, agent_state: AgentState, query: str, limit: int) -> str:
786
+ async def _search_files_native(self, agent_state: AgentState, query: str, limit: int) -> str:
662
787
  """Traditional search using existing passage manager."""
663
788
  # Get semantic search results
664
789
  passages = await self.agent_manager.query_source_passages_async(
letta/settings.py CHANGED
@@ -211,6 +211,9 @@ class Settings(BaseSettings):
211
211
  enable_keepalive: bool = Field(True, description="Enable keepalive messages in SSE streams to prevent timeouts")
212
212
  keepalive_interval: float = Field(50.0, description="Seconds between keepalive messages (default: 50)")
213
213
 
214
+ # SSE Streaming cancellation settings
215
+ enable_cancellation_aware_streaming: bool = Field(True, description="Enable cancellation aware streaming")
216
+
214
217
  # default handles
215
218
  default_llm_handle: Optional[str] = None
216
219
  default_embedding_handle: Optional[str] = None
@@ -303,6 +306,9 @@ class Settings(BaseSettings):
303
306
  tpuf_region: str = "gcp-us-central1"
304
307
  embed_all_messages: bool = False
305
308
 
309
+ # For encryption
310
+ encryption_key: Optional[str] = None
311
+
306
312
  # File processing timeout settings
307
313
  file_processing_timeout_minutes: int = 30
308
314
  file_processing_timeout_error_message: str = "File processing timed out after {} minutes. Please try again."
letta/utils.py CHANGED
@@ -17,7 +17,7 @@ from contextlib import contextmanager
17
17
  from datetime import datetime, timezone
18
18
  from functools import wraps
19
19
  from logging import Logger
20
- from typing import Any, Coroutine, Optional, Union, _GenericAlias, get_args, get_origin, get_type_hints
20
+ from typing import Any, Callable, Coroutine, Optional, Union, _GenericAlias, get_args, get_origin, get_type_hints
21
21
  from urllib.parse import urljoin, urlparse
22
22
 
23
23
  import demjson3 as demjson
@@ -1271,3 +1271,36 @@ def truncate_file_visible_content(visible_content: str, is_open: bool, per_file_
1271
1271
  visible_content += truncated_warning
1272
1272
 
1273
1273
  return visible_content
1274
+
1275
+
1276
+ def fire_and_forget(coro, task_name: Optional[str] = None, error_callback: Optional[Callable[[Exception], None]] = None) -> asyncio.Task:
1277
+ """
1278
+ Execute an async coroutine in the background without waiting for completion.
1279
+
1280
+ Args:
1281
+ coro: The coroutine to execute
1282
+ task_name: Optional name for logging purposes
1283
+ error_callback: Optional callback to execute if the task fails
1284
+
1285
+ Returns:
1286
+ The created asyncio Task object
1287
+ """
1288
+ import traceback
1289
+
1290
+ task = asyncio.create_task(coro)
1291
+
1292
+ def callback(t):
1293
+ try:
1294
+ t.result() # this re-raises exceptions from the task
1295
+ except Exception as e:
1296
+ task_desc = f"Background task {task_name}" if task_name else "Background task"
1297
+ logger.error(f"{task_desc} failed: {str(e)}\n{traceback.format_exc()}")
1298
+
1299
+ if error_callback:
1300
+ try:
1301
+ error_callback(e)
1302
+ except Exception as callback_error:
1303
+ logger.error(f"Error callback failed: {callback_error}")
1304
+
1305
+ task.add_done_callback(callback)
1306
+ return task
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: letta-nightly
3
- Version: 0.11.7.dev20250908104137
3
+ Version: 0.11.7.dev20250910104051
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  Author-email: Letta Team <contact@letta.com>
6
6
  License: Apache License
@@ -26,7 +26,7 @@ Requires-Dist: html2text>=2020.1.16
26
26
  Requires-Dist: httpx-sse>=0.4.0
27
27
  Requires-Dist: httpx>=0.28.0
28
28
  Requires-Dist: jinja2>=3.1.5
29
- Requires-Dist: letta-client==0.1.307
29
+ Requires-Dist: letta-client>=0.1.319
30
30
  Requires-Dist: llama-index-embeddings-openai>=0.3.1
31
31
  Requires-Dist: llama-index>=0.12.2
32
32
  Requires-Dist: markitdown[docx,pdf,pptx]>=0.1.2