letta-nightly 0.11.7.dev20250916104104__py3-none-any.whl → 0.11.7.dev20250917104122__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 (44) hide show
  1. letta/__init__.py +10 -2
  2. letta/adapters/letta_llm_request_adapter.py +0 -1
  3. letta/adapters/letta_llm_stream_adapter.py +0 -1
  4. letta/agent.py +1 -1
  5. letta/agents/letta_agent.py +1 -4
  6. letta/agents/letta_agent_v2.py +2 -1
  7. letta/agents/voice_agent.py +1 -1
  8. letta/helpers/converters.py +8 -2
  9. letta/helpers/crypto_utils.py +144 -0
  10. letta/llm_api/llm_api_tools.py +0 -1
  11. letta/llm_api/llm_client_base.py +0 -2
  12. letta/orm/__init__.py +1 -0
  13. letta/orm/agent.py +5 -1
  14. letta/orm/job.py +3 -1
  15. letta/orm/mcp_oauth.py +6 -0
  16. letta/orm/mcp_server.py +7 -1
  17. letta/orm/sqlalchemy_base.py +2 -1
  18. letta/schemas/agent.py +10 -7
  19. letta/schemas/job.py +10 -0
  20. letta/schemas/mcp.py +146 -6
  21. letta/schemas/provider_trace.py +0 -2
  22. letta/schemas/run.py +2 -0
  23. letta/schemas/secret.py +378 -0
  24. letta/serialize_schemas/marshmallow_agent.py +4 -0
  25. letta/server/rest_api/routers/v1/__init__.py +2 -0
  26. letta/server/rest_api/routers/v1/agents.py +9 -4
  27. letta/server/rest_api/routers/v1/archives.py +113 -0
  28. letta/server/rest_api/routers/v1/jobs.py +7 -2
  29. letta/server/rest_api/routers/v1/runs.py +9 -1
  30. letta/server/rest_api/routers/v1/tools.py +7 -26
  31. letta/services/agent_manager.py +17 -9
  32. letta/services/agent_serialization_manager.py +11 -3
  33. letta/services/archive_manager.py +73 -0
  34. letta/services/helpers/agent_manager_helper.py +6 -1
  35. letta/services/job_manager.py +18 -2
  36. letta/services/mcp_manager.py +198 -82
  37. letta/services/telemetry_manager.py +2 -0
  38. letta/services/tool_executor/composio_tool_executor.py +1 -1
  39. letta/services/tool_sandbox/base.py +2 -3
  40. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/METADATA +5 -2
  41. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/RECORD +44 -41
  42. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/WHEEL +0 -0
  43. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/entry_points.txt +0 -0
  44. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/licenses/LICENSE +0 -0
letta/__init__.py CHANGED
@@ -10,8 +10,16 @@ except PackageNotFoundError:
10
10
  if os.environ.get("LETTA_VERSION"):
11
11
  __version__ = os.environ["LETTA_VERSION"]
12
12
 
13
- # Import sqlite_functions early to ensure event handlers are registered
14
- from letta.orm import sqlite_functions
13
+ # Import sqlite_functions early to ensure event handlers are registered (only for SQLite)
14
+ # This is only needed for the server, not for client usage
15
+ try:
16
+ from letta.settings import DatabaseChoice, settings
17
+
18
+ if settings.database_engine == DatabaseChoice.SQLITE:
19
+ from letta.orm import sqlite_functions
20
+ except ImportError:
21
+ # If sqlite_vec is not installed, it's fine for client usage
22
+ pass
15
23
 
16
24
  # # imports for easier access
17
25
  from letta.schemas.agent import AgentState
@@ -106,7 +106,6 @@ class LettaLLMRequestAdapter(LettaLLMAdapter):
106
106
  request_json=self.request_data,
107
107
  response_json=self.response_data,
108
108
  step_id=step_id, # Use original step_id for telemetry
109
- organization_id=actor.organization_id,
110
109
  ),
111
110
  ),
112
111
  label="create_provider_trace",
@@ -164,7 +164,6 @@ class LettaLLMStreamAdapter(LettaLLMAdapter):
164
164
  },
165
165
  },
166
166
  step_id=step_id, # Use original step_id for telemetry
167
- organization_id=actor.organization_id,
168
167
  ),
169
168
  ),
170
169
  label="create_provider_trace",
letta/agent.py CHANGED
@@ -1628,7 +1628,7 @@ class Agent(BaseAgent):
1628
1628
  action_name = generate_composio_action_from_func_name(target_letta_tool.name)
1629
1629
  # Get entity ID from the agent_state
1630
1630
  entity_id = None
1631
- for env_var in self.agent_state.tool_exec_environment_variables:
1631
+ for env_var in self.agent_state.secrets:
1632
1632
  if env_var.key == COMPOSIO_ENTITY_ENV_VAR_KEY:
1633
1633
  entity_id = env_var.value
1634
1634
  # Get composio_api_key
@@ -405,7 +405,6 @@ class LettaAgent(BaseAgent):
405
405
  request_json=request_data,
406
406
  response_json=response_data,
407
407
  step_id=step_id, # Use original step_id for telemetry
408
- organization_id=self.actor.organization_id,
409
408
  ),
410
409
  )
411
410
  step_progression = StepProgression.LOGGED_TRACE
@@ -751,7 +750,6 @@ class LettaAgent(BaseAgent):
751
750
  request_json=request_data,
752
751
  response_json=response_data,
753
752
  step_id=step_id, # Use original step_id for telemetry
754
- organization_id=self.actor.organization_id,
755
753
  ),
756
754
  )
757
755
  step_progression = StepProgression.LOGGED_TRACE
@@ -1173,7 +1171,6 @@ class LettaAgent(BaseAgent):
1173
1171
  },
1174
1172
  },
1175
1173
  step_id=step_id, # Use original step_id for telemetry
1176
- organization_id=self.actor.organization_id,
1177
1174
  ),
1178
1175
  )
1179
1176
  step_progression = StepProgression.LOGGED_TRACE
@@ -1877,7 +1874,7 @@ class LettaAgent(BaseAgent):
1877
1874
  start_time = get_utc_timestamp_ns()
1878
1875
  agent_step_span.add_event(name="tool_execution_started")
1879
1876
 
1880
- sandbox_env_vars = {var.key: var.value for var in agent_state.tool_exec_environment_variables}
1877
+ sandbox_env_vars = {var.key: var.value for var in agent_state.secrets}
1881
1878
  tool_execution_manager = ToolExecutionManager(
1882
1879
  agent_state=agent_state,
1883
1880
  message_manager=self.message_manager,
@@ -1106,7 +1106,7 @@ class LettaAgentV2(BaseAgentV2):
1106
1106
  start_time = get_utc_timestamp_ns()
1107
1107
  agent_step_span.add_event(name="tool_execution_started")
1108
1108
 
1109
- sandbox_env_vars = {var.key: var.value for var in agent_state.tool_exec_environment_variables}
1109
+ sandbox_env_vars = {var.key: var.value for var in agent_state.secrets}
1110
1110
  tool_execution_manager = ToolExecutionManager(
1111
1111
  agent_state=agent_state,
1112
1112
  message_manager=self.message_manager,
@@ -1226,6 +1226,7 @@ class LettaAgentV2(BaseAgentV2):
1226
1226
  new_status=JobStatus.failed if is_error else JobStatus.completed,
1227
1227
  actor=self.actor,
1228
1228
  metadata=job_update_metadata,
1229
+ stop_reason=self.stop_reason.stop_reason if self.stop_reason else StopReasonType.error,
1229
1230
  )
1230
1231
  if request_span:
1231
1232
  request_span.end()
@@ -441,7 +441,7 @@ class VoiceAgent(BaseAgent):
441
441
  )
442
442
 
443
443
  # Use ToolExecutionManager for modern tool execution
444
- sandbox_env_vars = {var.key: var.value for var in agent_state.tool_exec_environment_variables}
444
+ sandbox_env_vars = {var.key: var.value for var in agent_state.secrets}
445
445
  tool_execution_manager = ToolExecutionManager(
446
446
  agent_state=agent_state,
447
447
  message_manager=self.message_manager,
@@ -44,8 +44,14 @@ from letta.schemas.tool_rule import (
44
44
  )
45
45
  from letta.settings import DatabaseChoice, settings
46
46
 
47
- if settings.database_engine == DatabaseChoice.SQLITE:
48
- import sqlite_vec
47
+ # Only import sqlite_vec if we're actually using SQLite database
48
+ # This is a runtime dependency only needed for SQLite vector operations
49
+ try:
50
+ if settings.database_engine == DatabaseChoice.SQLITE:
51
+ import sqlite_vec
52
+ except ImportError:
53
+ # If sqlite_vec is not installed, it's fine for client usage
54
+ pass
49
55
  # --------------------------
50
56
  # LLMConfig Serialization
51
57
  # --------------------------
@@ -0,0 +1,144 @@
1
+ import base64
2
+ import os
3
+ from typing import Optional
4
+
5
+ from cryptography.hazmat.backends import default_backend
6
+ from cryptography.hazmat.primitives import hashes
7
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
8
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
9
+
10
+ from letta.settings import settings
11
+
12
+
13
+ class CryptoUtils:
14
+ """Utility class for AES-256-GCM encryption/decryption of sensitive data."""
15
+
16
+ # AES-256 requires 32 bytes key
17
+ KEY_SIZE = 32
18
+ # GCM standard IV size is 12 bytes (96 bits)
19
+ IV_SIZE = 12
20
+ # GCM tag size is 16 bytes (128 bits)
21
+ TAG_SIZE = 16
22
+ # Salt size for key derivation
23
+ SALT_SIZE = 16
24
+
25
+ @classmethod
26
+ def _derive_key(cls, master_key: str, salt: bytes) -> bytes:
27
+ """Derive an AES key from the master key using PBKDF2."""
28
+ kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=cls.KEY_SIZE, salt=salt, iterations=100000, backend=default_backend())
29
+ return kdf.derive(master_key.encode())
30
+
31
+ @classmethod
32
+ def encrypt(cls, plaintext: str, master_key: Optional[str] = None) -> str:
33
+ """
34
+ Encrypt a string using AES-256-GCM.
35
+
36
+ Args:
37
+ plaintext: The string to encrypt
38
+ master_key: Optional master key (defaults to settings.encryption_key)
39
+
40
+ Returns:
41
+ Base64 encoded string containing: salt + iv + ciphertext + tag
42
+
43
+ Raises:
44
+ ValueError: If no encryption key is configured
45
+ """
46
+ if master_key is None:
47
+ master_key = settings.encryption_key
48
+
49
+ if not master_key:
50
+ raise ValueError("No encryption key configured. Set LETTA_ENCRYPTION_KEY environment variable.")
51
+
52
+ # Generate random salt and IV
53
+ salt = os.urandom(cls.SALT_SIZE)
54
+ iv = os.urandom(cls.IV_SIZE)
55
+
56
+ # Derive key from master key
57
+ key = cls._derive_key(master_key, salt)
58
+
59
+ # Create cipher
60
+ cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend())
61
+ encryptor = cipher.encryptor()
62
+
63
+ # Encrypt the plaintext
64
+ ciphertext = encryptor.update(plaintext.encode()) + encryptor.finalize()
65
+
66
+ # Get the authentication tag
67
+ tag = encryptor.tag
68
+
69
+ # Combine salt + iv + ciphertext + tag
70
+ encrypted_data = salt + iv + ciphertext + tag
71
+
72
+ # Return as base64 encoded string
73
+ return base64.b64encode(encrypted_data).decode("utf-8")
74
+
75
+ @classmethod
76
+ def decrypt(cls, encrypted: str, master_key: Optional[str] = None) -> str:
77
+ """
78
+ Decrypt a string that was encrypted using AES-256-GCM.
79
+
80
+ Args:
81
+ encrypted: Base64 encoded encrypted string
82
+ master_key: Optional master key (defaults to settings.encryption_key)
83
+
84
+ Returns:
85
+ The decrypted plaintext string
86
+
87
+ Raises:
88
+ ValueError: If no encryption key is configured or decryption fails
89
+ """
90
+ if master_key is None:
91
+ master_key = settings.encryption_key
92
+
93
+ if not master_key:
94
+ raise ValueError("No encryption key configured. Set LETTA_ENCRYPTION_KEY environment variable.")
95
+
96
+ try:
97
+ # Decode from base64
98
+ encrypted_data = base64.b64decode(encrypted)
99
+
100
+ # Extract components
101
+ salt = encrypted_data[: cls.SALT_SIZE]
102
+ iv = encrypted_data[cls.SALT_SIZE : cls.SALT_SIZE + cls.IV_SIZE]
103
+ ciphertext = encrypted_data[cls.SALT_SIZE + cls.IV_SIZE : -cls.TAG_SIZE]
104
+ tag = encrypted_data[-cls.TAG_SIZE :]
105
+
106
+ # Derive key from master key
107
+ key = cls._derive_key(master_key, salt)
108
+
109
+ # Create cipher
110
+ cipher = Cipher(algorithms.AES(key), modes.GCM(iv, tag), backend=default_backend())
111
+ decryptor = cipher.decryptor()
112
+
113
+ # Decrypt the ciphertext
114
+ plaintext = decryptor.update(ciphertext) + decryptor.finalize()
115
+
116
+ return plaintext.decode("utf-8")
117
+
118
+ except Exception as e:
119
+ raise ValueError(f"Failed to decrypt data: {str(e)}")
120
+
121
+ @classmethod
122
+ def is_encrypted(cls, value: str) -> bool:
123
+ """
124
+ Check if a string appears to be encrypted (base64 encoded with correct size).
125
+
126
+ This is a heuristic check and may have false positives.
127
+ """
128
+ try:
129
+ decoded = base64.b64decode(value)
130
+ # Check if length is consistent with our encryption format
131
+ # Minimum size: salt(16) + iv(12) + tag(16) + at least 1 byte of ciphertext
132
+ return len(decoded) >= cls.SALT_SIZE + cls.IV_SIZE + cls.TAG_SIZE + 1
133
+ except Exception:
134
+ return False
135
+
136
+ @classmethod
137
+ def is_encryption_available(cls) -> bool:
138
+ """
139
+ Check if encryption is available (encryption key is configured).
140
+
141
+ Returns:
142
+ True if encryption key is configured, False otherwise
143
+ """
144
+ return bool(settings.encryption_key)
@@ -235,7 +235,6 @@ def create(
235
235
  request_json=prepare_openai_payload(data),
236
236
  response_json=response.model_json_schema(),
237
237
  step_id=step_id,
238
- organization_id=actor.organization_id,
239
238
  ),
240
239
  )
241
240
 
@@ -64,7 +64,6 @@ class LLMClientBase:
64
64
  request_json=request_data,
65
65
  response_json=response_data,
66
66
  step_id=step_id,
67
- organization_id=self.actor.organization_id,
68
67
  ),
69
68
  )
70
69
  log_event(name="llm_response_received", attributes=response_data)
@@ -98,7 +97,6 @@ class LLMClientBase:
98
97
  request_json=request_data,
99
98
  response_json=response_data,
100
99
  step_id=step_id,
101
- organization_id=self.actor.organization_id,
102
100
  ),
103
101
  )
104
102
 
letta/orm/__init__.py CHANGED
@@ -18,6 +18,7 @@ from letta.orm.job import Job
18
18
  from letta.orm.job_messages import JobMessage
19
19
  from letta.orm.llm_batch_items import LLMBatchItem
20
20
  from letta.orm.llm_batch_job import LLMBatchJob
21
+ from letta.orm.mcp_oauth import MCPOAuth
21
22
  from letta.orm.mcp_server import MCPServer
22
23
  from letta.orm.message import Message
23
24
  from letta.orm.organization import Organization
letta/orm/agent.py CHANGED
@@ -233,6 +233,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
233
233
  "identity_ids": [],
234
234
  "multi_agent_group": None,
235
235
  "tool_exec_environment_variables": [],
236
+ "secrets": [],
236
237
  }
237
238
 
238
239
  # Optional fields: only included if requested
@@ -253,6 +254,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
253
254
  "identity_ids": lambda: [i.id for i in self.identities],
254
255
  "multi_agent_group": lambda: self.multi_agent_group,
255
256
  "tool_exec_environment_variables": lambda: self.tool_exec_environment_variables,
257
+ "secrets": lambda: self.tool_exec_environment_variables,
256
258
  }
257
259
 
258
260
  include_relationships = set(optional_fields.keys() if include_relationships is None else include_relationships)
@@ -324,6 +326,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
324
326
  "identity_ids": [],
325
327
  "multi_agent_group": None,
326
328
  "tool_exec_environment_variables": [],
329
+ "secrets": [],
327
330
  }
328
331
 
329
332
  # Initialize include_relationships to an empty set if it's None
@@ -344,7 +347,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
344
347
  multi_agent_group = self.awaitable_attrs.multi_agent_group if "multi_agent_group" in include_relationships else none_async()
345
348
  tool_exec_environment_variables = (
346
349
  self.awaitable_attrs.tool_exec_environment_variables
347
- if "tool_exec_environment_variables" in include_relationships
350
+ if "tool_exec_environment_variables" in include_relationships or "secrets" in include_relationships
348
351
  else empty_list_async()
349
352
  )
350
353
  file_agents = self.awaitable_attrs.file_agents if "memory" in include_relationships else empty_list_async()
@@ -368,5 +371,6 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
368
371
  state["identity_ids"] = [i.id for i in identities]
369
372
  state["multi_agent_group"] = multi_agent_group
370
373
  state["tool_exec_environment_variables"] = tool_exec_environment_variables
374
+ state["secrets"] = tool_exec_environment_variables
371
375
 
372
376
  return self.__pydantic_model__(**state)
letta/orm/job.py CHANGED
@@ -1,13 +1,14 @@
1
1
  from datetime import datetime
2
2
  from typing import TYPE_CHECKING, List, Optional
3
3
 
4
- from sqlalchemy import JSON, BigInteger, ForeignKey, Index, String
4
+ from sqlalchemy import JSON, BigInteger, Boolean, ForeignKey, Index, String
5
5
  from sqlalchemy.orm import Mapped, mapped_column, relationship
6
6
 
7
7
  from letta.orm.mixins import UserMixin
8
8
  from letta.orm.sqlalchemy_base import SqlalchemyBase
9
9
  from letta.schemas.enums import JobStatus, JobType
10
10
  from letta.schemas.job import Job as PydanticJob, LettaRequestConfig
11
+ from letta.schemas.letta_stop_reason import StopReasonType
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from letta.orm.job_messages import JobMessage
@@ -28,6 +29,7 @@ class Job(SqlalchemyBase, UserMixin):
28
29
 
29
30
  status: Mapped[JobStatus] = mapped_column(String, default=JobStatus.created, doc="The current status of the job.")
30
31
  completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True, doc="The unix timestamp of when the job was completed.")
32
+ stop_reason: Mapped[Optional[StopReasonType]] = mapped_column(String, nullable=True, doc="The reason why the job was stopped.")
31
33
  metadata_: Mapped[Optional[dict]] = mapped_column(JSON, doc="The metadata of the job.")
32
34
  job_type: Mapped[JobType] = mapped_column(
33
35
  String,
letta/orm/mcp_oauth.py CHANGED
@@ -38,7 +38,11 @@ class MCPOAuth(SqlalchemyBase, OrganizationMixin, UserMixin):
38
38
 
39
39
  # Token data
40
40
  access_token: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth access token")
41
+ access_token_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted OAuth access token")
42
+
41
43
  refresh_token: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth refresh token")
44
+ refresh_token_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted OAuth refresh token")
45
+
42
46
  token_type: Mapped[str] = mapped_column(String(50), default="Bearer", doc="Token type")
43
47
  expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True, doc="Token expiry time")
44
48
  scope: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth scope")
@@ -46,6 +50,8 @@ class MCPOAuth(SqlalchemyBase, OrganizationMixin, UserMixin):
46
50
  # Client configuration
47
51
  client_id: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth client ID")
48
52
  client_secret: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth client secret")
53
+ client_secret_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted OAuth client secret")
54
+
49
55
  redirect_uri: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth redirect URI")
50
56
 
51
57
  # Session state
letta/orm/mcp_server.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING, Optional
2
2
 
3
- from sqlalchemy import JSON, String, UniqueConstraint
3
+ from sqlalchemy import JSON, String, Text, UniqueConstraint
4
4
  from sqlalchemy.orm import Mapped, mapped_column
5
5
 
6
6
  from letta.functions.mcp_client.types import StdioServerConfig
@@ -39,9 +39,15 @@ class MCPServer(SqlalchemyBase, OrganizationMixin):
39
39
  # access token / api key for MCP servers that require authentication
40
40
  token: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The access token or api key for the MCP server")
41
41
 
42
+ # encrypted access token or api key for the MCP server
43
+ token_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted access token or api key for the MCP server")
44
+
42
45
  # custom headers for authentication (key-value pairs)
43
46
  custom_headers: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, doc="Custom authentication headers as key-value pairs")
44
47
 
48
+ # encrypted custom headers for authentication (key-value pairs)
49
+ custom_headers_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted custom authentication headers")
50
+
45
51
  # stdio server
46
52
  stdio_config: Mapped[Optional[StdioServerConfig]] = mapped_column(
47
53
  MCPStdioServerConfigColumn, nullable=True, doc="The configuration for the stdio server"
@@ -14,7 +14,6 @@ from sqlalchemy.orm.interfaces import ORMOption
14
14
  from letta.log import get_logger
15
15
  from letta.orm.base import Base, CommonSqlalchemyMetaMixins
16
16
  from letta.orm.errors import DatabaseTimeoutError, ForeignKeyConstraintViolationError, NoResultFound, UniqueConstraintViolationError
17
- from letta.orm.sqlite_functions import adapt_array
18
17
  from letta.settings import DatabaseChoice
19
18
 
20
19
  if TYPE_CHECKING:
@@ -401,6 +400,8 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
401
400
  query = query.order_by(cls.embedding.cosine_distance(query_embedding).asc())
402
401
  else:
403
402
  # SQLite with custom vector type
403
+ from letta.orm.sqlite_functions import adapt_array
404
+
404
405
  query_embedding_binary = adapt_array(query_embedding)
405
406
  query = query.order_by(
406
407
  func.cosine_distance(cls.embedding, query_embedding_binary).asc(),
letta/schemas/agent.py CHANGED
@@ -86,6 +86,11 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
86
86
  sources: List[Source] = Field(..., description="The sources used by the agent.")
87
87
  tags: List[str] = Field(..., description="The tags associated with the agent.")
88
88
  tool_exec_environment_variables: List[AgentEnvironmentVariable] = Field(
89
+ default_factory=list,
90
+ description="Deprecated: use `secrets` field instead.",
91
+ deprecated=True,
92
+ )
93
+ secrets: List[AgentEnvironmentVariable] = Field(
89
94
  default_factory=list, description="The environment variables for tool execution specific to this agent."
90
95
  )
91
96
  project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
@@ -133,7 +138,7 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
133
138
  def get_agent_env_vars_as_dict(self) -> Dict[str, str]:
134
139
  # Get environment variables for this agent specifically
135
140
  per_agent_env_vars = {}
136
- for agent_env_var_obj in self.tool_exec_environment_variables:
141
+ for agent_env_var_obj in self.secrets:
137
142
  per_agent_env_vars[agent_env_var_obj.key] = agent_env_var_obj.value
138
143
  return per_agent_env_vars
139
144
 
@@ -222,9 +227,8 @@ class CreateAgent(BaseModel, validate_assignment=True): #
222
227
  deprecated=True,
223
228
  description="Deprecated: Project should now be passed via the X-Project header instead of in the request body. If using the sdk, this can be done via the new x_project field below.",
224
229
  )
225
- tool_exec_environment_variables: Optional[Dict[str, str]] = Field(
226
- None, description="The environment variables for tool execution specific to this agent."
227
- )
230
+ tool_exec_environment_variables: Optional[Dict[str, str]] = Field(None, description="Deprecated: use `secrets` field instead.")
231
+ secrets: Optional[Dict[str, str]] = Field(None, description="The environment variables for tool execution specific to this agent.")
228
232
  memory_variables: Optional[Dict[str, str]] = Field(None, description="The variables that should be set for the agent.")
229
233
  project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
230
234
  template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
@@ -328,9 +332,8 @@ class UpdateAgent(BaseModel):
328
332
  message_ids: Optional[List[str]] = Field(None, description="The ids of the messages in the agent's in-context memory.")
329
333
  description: Optional[str] = Field(None, description="The description of the agent.")
330
334
  metadata: Optional[Dict] = Field(None, description="The metadata of the agent.")
331
- tool_exec_environment_variables: Optional[Dict[str, str]] = Field(
332
- None, description="The environment variables for tool execution specific to this agent."
333
- )
335
+ tool_exec_environment_variables: Optional[Dict[str, str]] = Field(None, description="Deprecated: use `secrets` field instead")
336
+ secrets: Optional[Dict[str, str]] = Field(None, description="The environment variables for tool execution specific to this agent.")
334
337
  project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
335
338
  template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
336
339
  base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
letta/schemas/job.py CHANGED
@@ -8,16 +8,26 @@ from letta.helpers.datetime_helpers import get_utc_time
8
8
  from letta.schemas.enums import JobStatus, JobType
9
9
  from letta.schemas.letta_base import OrmMetadataBase
10
10
  from letta.schemas.letta_message import MessageType
11
+ from letta.schemas.letta_stop_reason import StopReasonType
11
12
 
12
13
 
13
14
  class JobBase(OrmMetadataBase):
14
15
  __id_prefix__ = "job"
15
16
  status: JobStatus = Field(default=JobStatus.created, description="The status of the job.")
16
17
  created_at: datetime = Field(default_factory=get_utc_time, description="The unix timestamp of when the job was created.")
18
+
19
+ # completion related
17
20
  completed_at: Optional[datetime] = Field(None, description="The unix timestamp of when the job was completed.")
21
+ stop_reason: Optional[StopReasonType] = Field(None, description="The reason why the job was stopped.")
22
+
23
+ # metadata
18
24
  metadata: Optional[dict] = Field(None, validation_alias="metadata_", description="The metadata of the job.")
19
25
  job_type: JobType = Field(default=JobType.JOB, description="The type of the job.")
20
26
 
27
+ ## TODO: Run-specific fields
28
+ # background: Optional[bool] = Field(None, description="Whether the job was created in background mode.")
29
+ # agent_id: Optional[str] = Field(None, description="The agent associated with this job/run.")
30
+
21
31
  callback_url: Optional[str] = Field(None, description="If set, POST to this URL when the job completes.")
22
32
  callback_sent_at: Optional[datetime] = Field(None, description="Timestamp when the callback was last attempted.")
23
33
  callback_status_code: Optional[int] = Field(None, description="HTTP status code returned by the callback endpoint.")