letta-nightly 0.11.7.dev20250916104104__py3-none-any.whl → 0.11.7.dev20250918104055__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 (63) 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 +4 -4
  5. letta/agents/agent_loop.py +2 -1
  6. letta/agents/base_agent.py +1 -1
  7. letta/agents/letta_agent.py +1 -4
  8. letta/agents/letta_agent_v2.py +5 -4
  9. letta/agents/temporal/activities/__init__.py +4 -0
  10. letta/agents/temporal/activities/example_activity.py +7 -0
  11. letta/agents/temporal/activities/prepare_messages.py +10 -0
  12. letta/agents/temporal/temporal_agent_workflow.py +56 -0
  13. letta/agents/temporal/types.py +25 -0
  14. letta/agents/voice_agent.py +3 -3
  15. letta/helpers/converters.py +8 -2
  16. letta/helpers/crypto_utils.py +144 -0
  17. letta/llm_api/llm_api_tools.py +0 -1
  18. letta/llm_api/llm_client_base.py +0 -2
  19. letta/orm/__init__.py +1 -0
  20. letta/orm/agent.py +9 -4
  21. letta/orm/job.py +3 -1
  22. letta/orm/mcp_oauth.py +6 -0
  23. letta/orm/mcp_server.py +7 -1
  24. letta/orm/sqlalchemy_base.py +2 -1
  25. letta/prompts/prompt_generator.py +4 -4
  26. letta/schemas/agent.py +14 -200
  27. letta/schemas/enums.py +15 -0
  28. letta/schemas/job.py +10 -0
  29. letta/schemas/mcp.py +146 -6
  30. letta/schemas/memory.py +216 -103
  31. letta/schemas/provider_trace.py +0 -2
  32. letta/schemas/run.py +2 -0
  33. letta/schemas/secret.py +378 -0
  34. letta/schemas/step.py +5 -1
  35. letta/schemas/tool_rule.py +34 -44
  36. letta/serialize_schemas/marshmallow_agent.py +4 -0
  37. letta/server/rest_api/routers/v1/__init__.py +2 -0
  38. letta/server/rest_api/routers/v1/agents.py +9 -4
  39. letta/server/rest_api/routers/v1/archives.py +113 -0
  40. letta/server/rest_api/routers/v1/jobs.py +7 -2
  41. letta/server/rest_api/routers/v1/runs.py +9 -1
  42. letta/server/rest_api/routers/v1/steps.py +29 -0
  43. letta/server/rest_api/routers/v1/tools.py +7 -26
  44. letta/server/server.py +2 -2
  45. letta/services/agent_manager.py +21 -15
  46. letta/services/agent_serialization_manager.py +11 -3
  47. letta/services/archive_manager.py +73 -0
  48. letta/services/helpers/agent_manager_helper.py +10 -5
  49. letta/services/job_manager.py +18 -2
  50. letta/services/mcp_manager.py +198 -82
  51. letta/services/step_manager.py +26 -0
  52. letta/services/summarizer/summarizer.py +25 -3
  53. letta/services/telemetry_manager.py +2 -0
  54. letta/services/tool_executor/composio_tool_executor.py +1 -1
  55. letta/services/tool_executor/sandbox_tool_executor.py +2 -2
  56. letta/services/tool_sandbox/base.py +135 -9
  57. letta/settings.py +2 -2
  58. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/METADATA +6 -3
  59. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/RECORD +62 -55
  60. letta/templates/template_helper.py +0 -53
  61. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/WHEEL +0 -0
  62. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/entry_points.txt +0 -0
  63. {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/licenses/LICENSE +0 -0
letta/schemas/memory.py CHANGED
@@ -1,20 +1,17 @@
1
1
  import asyncio
2
2
  import logging
3
3
  from datetime import datetime
4
- from typing import TYPE_CHECKING, List, Optional
5
-
6
- from jinja2 import Template, TemplateSyntaxError
7
- from pydantic import BaseModel, Field, field_validator
8
-
9
- # Forward referencing to avoid circular import with Agent -> Memory -> Agent
10
- if TYPE_CHECKING:
11
- pass
4
+ from io import StringIO
5
+ from typing import TYPE_CHECKING, List, Optional, Union
12
6
 
13
7
  from openai.types.beta.function_tool import FunctionTool as OpenAITool
8
+ from pydantic import BaseModel, Field, field_validator
14
9
 
15
- from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
10
+ from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT, CORE_MEMORY_LINE_NUMBER_WARNING
16
11
  from letta.otel.tracing import trace_method
17
12
  from letta.schemas.block import Block, FileBlock
13
+ from letta.schemas.enums import AgentType
14
+ from letta.schemas.file import FileStatus
18
15
  from letta.schemas.message import Message
19
16
 
20
17
 
@@ -23,12 +20,9 @@ class ContextWindowOverview(BaseModel):
23
20
  Overview of the context window, including the number of messages and tokens.
24
21
  """
25
22
 
26
- # top-level information
27
23
  context_window_size_max: int = Field(..., description="The maximum amount of tokens the context window can hold.")
28
24
  context_window_size_current: int = Field(..., description="The current number of tokens in the context window.")
29
25
 
30
- # context window breakdown (in messages)
31
- # (technically not in the context window, but useful to know)
32
26
  num_messages: int = Field(..., description="The number of messages in the context window.")
33
27
  num_archival_memory: int = Field(..., description="The number of messages in the archival memory.")
34
28
  num_recall_memory: int = Field(..., description="The number of messages in the recall memory.")
@@ -39,9 +33,6 @@ class ContextWindowOverview(BaseModel):
39
33
  ..., description="The metadata summary of the external memory sources (archival + recall metadata)."
40
34
  )
41
35
 
42
- # context window breakdown (in tokens)
43
- # this should all add up to context_window_size_current
44
-
45
36
  num_tokens_system: int = Field(..., description="The number of tokens in the system prompt.")
46
37
  system_prompt: str = Field(..., description="The content of the system prompt.")
47
38
 
@@ -55,8 +46,6 @@ class ContextWindowOverview(BaseModel):
55
46
  functions_definitions: Optional[List[OpenAITool]] = Field(..., description="The content of the functions definitions.")
56
47
 
57
48
  num_tokens_messages: int = Field(..., description="The number of tokens in the messages list.")
58
- # TODO make list of messages?
59
- # messages: List[dict] = Field(..., description="The messages in the context window.")
60
49
  messages: List[Message] = Field(..., description="The messages in the context window.")
61
50
 
62
51
 
@@ -67,7 +56,7 @@ class Memory(BaseModel, validate_assignment=True):
67
56
 
68
57
  """
69
58
 
70
- # Memory.block contains the list of memory blocks in the core memory
59
+ agent_type: Optional[Union["AgentType", str]] = Field(None, description="Agent type controlling prompt rendering.")
71
60
  blocks: List[Block] = Field(..., description="Memory blocks contained in the agent's in-context memory")
72
61
  file_blocks: List[FileBlock] = Field(
73
62
  default_factory=list, description="Special blocks representing the agent's in-context memory of an attached file"
@@ -97,111 +86,238 @@ class Memory(BaseModel, validate_assignment=True):
97
86
 
98
87
  return unique_blocks
99
88
 
100
- # Memory.template is a Jinja2 template for compiling memory module into a prompt string.
101
- prompt_template: str = Field(
102
- default="{% for block in blocks %}"
103
- "<{{ block.label }}>\n"
104
- "<metadata>"
105
- 'read_only="{{ block.read_only}}" chars_current="{{ block.value|length }}" chars_limit="{{ block.limit }}"'
106
- "</metadata>"
107
- "<value>"
108
- "{{ block.value }}\n"
109
- "</value>"
110
- "</{{ block.label }}>\n"
111
- "{% if not loop.last %}\n{% endif %}"
112
- "{% endfor %}",
113
- description="Jinja2 template for compiling memory blocks into a prompt string",
114
- )
89
+ prompt_template: str = Field(default="", description="Deprecated. Ignored for performance.")
115
90
 
116
91
  def get_prompt_template(self) -> str:
117
- """Return the current Jinja2 template string."""
92
+ """Return the stored (deprecated) prompt template string."""
118
93
  return str(self.prompt_template)
119
94
 
120
95
  @trace_method
121
96
  def set_prompt_template(self, prompt_template: str):
122
- """
123
- Set a new Jinja2 template string.
124
- Validates the template syntax and compatibility with current memory structure.
125
- """
126
- try:
127
- # Validate Jinja2 syntax
128
- Template(prompt_template)
129
-
130
- # Validate compatibility with current memory structure
131
- Template(prompt_template).render(blocks=self.blocks, file_blocks=self.file_blocks, sources=[], max_files_open=None)
132
-
133
- # If we get here, the template is valid and compatible
134
- self.prompt_template = prompt_template
135
- except TemplateSyntaxError as e:
136
- raise ValueError(f"Invalid Jinja2 template syntax: {str(e)}")
137
- except Exception as e:
138
- raise ValueError(f"Prompt template is not compatible with current memory structure: {str(e)}")
97
+ """Deprecated. Stores the provided string but is not used for rendering."""
98
+ self.prompt_template = prompt_template
139
99
 
140
100
  @trace_method
141
101
  async def set_prompt_template_async(self, prompt_template: str):
142
- """
143
- Async version of set_prompt_template that doesn't block the event loop.
144
- """
145
- try:
146
- # Validate Jinja2 syntax with async enabled
147
- Template(prompt_template)
148
-
149
- # Validate compatibility with current memory structure - use async rendering
150
- template = Template(prompt_template)
151
- await asyncio.to_thread(template.render, blocks=self.blocks, file_blocks=self.file_blocks, sources=[], max_files_open=None)
152
-
153
- # If we get here, the template is valid and compatible
154
- self.prompt_template = prompt_template
155
- except TemplateSyntaxError as e:
156
- raise ValueError(f"Invalid Jinja2 template syntax: {str(e)}")
157
- except Exception as e:
158
- raise ValueError(f"Prompt template is not compatible with current memory structure: {str(e)}")
102
+ """Deprecated. Async setter that stores the string but does not validate or use it."""
103
+ self.prompt_template = prompt_template
159
104
 
160
105
  @trace_method
106
+ def _render_memory_blocks_standard(self, s: StringIO):
107
+ if len(self.blocks) == 0:
108
+ # s.write("<memory_blocks></memory_blocks>") # TODO: consider empty tags
109
+ s.write("")
110
+ return
111
+
112
+ s.write("<memory_blocks>\nThe following memory blocks are currently engaged in your core memory unit:\n\n")
113
+ for idx, block in enumerate(self.blocks):
114
+ label = block.label or "block"
115
+ value = block.value or ""
116
+ desc = block.description or ""
117
+ chars_current = len(value)
118
+ limit = block.limit if block.limit is not None else 0
119
+
120
+ s.write(f"<{label}>\n")
121
+ s.write("<description>\n")
122
+ s.write(f"{desc}\n")
123
+ s.write("</description>\n")
124
+ s.write("<metadata>")
125
+ if getattr(block, "read_only", False):
126
+ s.write("\n- read_only=true")
127
+ s.write(f"\n- chars_current={chars_current}")
128
+ s.write(f"\n- chars_limit={limit}\n")
129
+ s.write("</metadata>\n")
130
+ s.write("<value>\n")
131
+ s.write(f"{value}\n")
132
+ s.write("</value>\n")
133
+ s.write(f"</{label}>\n")
134
+ if idx != len(self.blocks) - 1:
135
+ s.write("\n")
136
+ s.write("\n</memory_blocks>")
137
+
138
+ def _render_memory_blocks_line_numbered(self, s: StringIO):
139
+ s.write("<memory_blocks>\nThe following memory blocks are currently engaged in your core memory unit:\n\n")
140
+ for idx, block in enumerate(self.blocks):
141
+ label = block.label or "block"
142
+ value = block.value or ""
143
+ desc = block.description or ""
144
+ limit = block.limit if block.limit is not None else 0
145
+
146
+ s.write(f"<{label}>\n")
147
+ s.write("<description>\n")
148
+ s.write(f"{desc}\n")
149
+ s.write("</description>\n")
150
+ s.write("<metadata>")
151
+ if getattr(block, "read_only", False):
152
+ s.write("\n- read_only=true")
153
+ s.write(f"\n- chars_current={len(value)}")
154
+ s.write(f"\n- chars_limit={limit}\n")
155
+ s.write("</metadata>\n")
156
+ s.write("<value>\n")
157
+ s.write(f"{CORE_MEMORY_LINE_NUMBER_WARNING}\n")
158
+ if value:
159
+ for i, line in enumerate(value.split("\n"), start=1):
160
+ s.write(f"Line {i}: {line}\n")
161
+ s.write("</value>\n")
162
+ s.write(f"</{label}>\n")
163
+ if idx != len(self.blocks) - 1:
164
+ s.write("\n")
165
+ s.write("\n</memory_blocks>")
166
+
167
+ def _render_directories_common(self, s: StringIO, sources, max_files_open):
168
+ s.write("\n\n<directories>\n")
169
+ if max_files_open is not None:
170
+ current_open = sum(1 for b in self.file_blocks if getattr(b, "value", None))
171
+ s.write("<file_limits>\n")
172
+ s.write(f"- current_files_open={current_open}\n")
173
+ s.write(f"- max_files_open={max_files_open}\n")
174
+ s.write("</file_limits>\n")
175
+
176
+ for source in sources:
177
+ source_name = getattr(source, "name", "")
178
+ source_desc = getattr(source, "description", None)
179
+ source_instr = getattr(source, "instructions", None)
180
+ source_id = getattr(source, "id", None)
181
+
182
+ s.write(f'<directory name="{source_name}">\n')
183
+ if source_desc:
184
+ s.write(f"<description>{source_desc}</description>\n")
185
+ if source_instr:
186
+ s.write(f"<instructions>{source_instr}</instructions>\n")
187
+
188
+ if self.file_blocks:
189
+ for fb in self.file_blocks:
190
+ if source_id is not None and getattr(fb, "source_id", None) == source_id:
191
+ status = FileStatus.open.value if getattr(fb, "value", None) else FileStatus.closed.value
192
+ label = fb.label or "file"
193
+ desc = fb.description or ""
194
+ chars_current = len(fb.value or "")
195
+ limit = fb.limit if fb.limit is not None else 0
196
+
197
+ s.write(f'<file status="{status}" name="{label}">\n')
198
+ if desc:
199
+ s.write("<description>\n")
200
+ s.write(f"{desc}\n")
201
+ s.write("</description>\n")
202
+ s.write("<metadata>")
203
+ if getattr(fb, "read_only", False):
204
+ s.write("\n- read_only=true")
205
+ s.write(f"\n- chars_current={chars_current}\n")
206
+ s.write(f"- chars_limit={limit}\n")
207
+ s.write("</metadata>\n")
208
+ if getattr(fb, "value", None):
209
+ s.write("<value>\n")
210
+ s.write(f"{fb.value}\n")
211
+ s.write("</value>\n")
212
+ s.write("</file>\n")
213
+
214
+ s.write("</directory>\n")
215
+ s.write("</directories>")
216
+
217
+ def _render_directories_react(self, s: StringIO, sources, max_files_open):
218
+ s.write("\n\n<directories>\n")
219
+ if max_files_open is not None:
220
+ current_open = sum(1 for b in self.file_blocks if getattr(b, "value", None))
221
+ s.write("<file_limits>\n")
222
+ s.write(f"- current_files_open={current_open}\n")
223
+ s.write(f"- max_files_open={max_files_open}\n")
224
+ s.write("</file_limits>\n")
225
+
226
+ for source in sources:
227
+ source_name = getattr(source, "name", "")
228
+ source_desc = getattr(source, "description", None)
229
+ source_instr = getattr(source, "instructions", None)
230
+ source_id = getattr(source, "id", None)
231
+
232
+ s.write(f'<directory name="{source_name}">\n')
233
+ if source_desc:
234
+ s.write(f"<description>{source_desc}</description>\n")
235
+ if source_instr:
236
+ s.write(f"<instructions>{source_instr}</instructions>\n")
237
+
238
+ if self.file_blocks:
239
+ for fb in self.file_blocks:
240
+ if source_id is not None and getattr(fb, "source_id", None) == source_id:
241
+ status = FileStatus.open.value if getattr(fb, "value", None) else FileStatus.closed.value
242
+ label = fb.label or "file"
243
+ desc = fb.description or ""
244
+ chars_current = len(fb.value or "")
245
+ limit = fb.limit if fb.limit is not None else 0
246
+
247
+ s.write(f'<file status="{status}">\n')
248
+ s.write(f"<{label}>\n")
249
+ s.write("<description>\n")
250
+ s.write(f"{desc}\n")
251
+ s.write("</description>\n")
252
+ s.write("<metadata>")
253
+ if getattr(fb, "read_only", False):
254
+ s.write("\n- read_only=true")
255
+ s.write(f"\n- chars_current={chars_current}\n")
256
+ s.write(f"- chars_limit={limit}\n")
257
+ s.write("</metadata>\n")
258
+ s.write("<value>\n")
259
+ s.write(f"{fb.value or ''}\n")
260
+ s.write("</value>\n")
261
+ s.write(f"</{label}>\n")
262
+ s.write("</file>\n")
263
+
264
+ s.write("</directory>\n")
265
+ s.write("</directories>")
266
+
161
267
  def compile(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
162
- """Generate a string representation of the memory in-context using the Jinja2 template"""
163
- try:
164
- template = Template(self.prompt_template)
165
- return template.render(
166
- blocks=self.blocks,
167
- file_blocks=self.file_blocks,
168
- tool_usage_rules=tool_usage_rules,
169
- sources=sources,
170
- max_files_open=max_files_open,
171
- )
172
- except TemplateSyntaxError as e:
173
- raise ValueError(f"Invalid Jinja2 template syntax: {str(e)}")
174
- except Exception as e:
175
- raise ValueError(f"Prompt template is not compatible with current memory structure: {str(e)}")
268
+ """Efficiently render memory, tool rules, and sources into a prompt string."""
269
+ s = StringIO()
270
+
271
+ raw_type = self.agent_type.value if hasattr(self.agent_type, "value") else (self.agent_type or "")
272
+ norm_type = raw_type.lower()
273
+ is_react = norm_type in ("react_agent", "workflow_agent")
274
+ is_line_numbered = norm_type in ("sleeptime_agent", "memgpt_v2_agent")
275
+
276
+ # Memory blocks (not for react/workflow). Always include wrapper for preview/tests.
277
+ if not is_react:
278
+ if is_line_numbered:
279
+ self._render_memory_blocks_line_numbered(s)
280
+ else:
281
+ self._render_memory_blocks_standard(s)
282
+
283
+ if tool_usage_rules is not None:
284
+ desc = getattr(tool_usage_rules, "description", None) or ""
285
+ val = getattr(tool_usage_rules, "value", None) or ""
286
+ s.write("\n\n<tool_usage_rules>\n")
287
+ s.write(f"{desc}\n\n")
288
+ s.write(f"{val}\n")
289
+ s.write("</tool_usage_rules>")
290
+
291
+ if sources:
292
+ if is_react:
293
+ self._render_directories_react(s, sources, max_files_open)
294
+ else:
295
+ self._render_directories_common(s, sources, max_files_open)
296
+
297
+ return s.getvalue()
176
298
 
177
299
  @trace_method
178
300
  async def compile_async(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
179
- """Async version of compile that doesn't block the event loop"""
180
- try:
181
- template = Template(self.prompt_template, enable_async=True)
182
- return await template.render_async(
183
- blocks=self.blocks,
184
- file_blocks=self.file_blocks,
185
- tool_usage_rules=tool_usage_rules,
186
- sources=sources,
187
- max_files_open=max_files_open,
188
- )
189
- except TemplateSyntaxError as e:
190
- raise ValueError(f"Invalid Jinja2 template syntax: {str(e)}")
191
- except Exception as e:
192
- raise ValueError(f"Prompt template is not compatible with current memory structure: {str(e)}")
301
+ """Async version that offloads to a thread for CPU-bound string building."""
302
+ return await asyncio.to_thread(
303
+ self.compile,
304
+ tool_usage_rules=tool_usage_rules,
305
+ sources=sources,
306
+ max_files_open=max_files_open,
307
+ )
193
308
 
194
309
  @trace_method
195
310
  async def compile_in_thread_async(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
196
- """Compile the memory in a thread"""
197
- return await asyncio.to_thread(self.compile, tool_usage_rules=tool_usage_rules, sources=sources, max_files_open=max_files_open)
311
+ """Deprecated: use compile() instead."""
312
+ import warnings
313
+
314
+ warnings.warn("compile_in_thread_async is deprecated; use compile()", DeprecationWarning, stacklevel=2)
315
+ return self.compile(tool_usage_rules=tool_usage_rules, sources=sources, max_files_open=max_files_open)
198
316
 
199
317
  def list_block_labels(self) -> List[str]:
200
318
  """Return a list of the block names held inside the memory object"""
201
- # return list(self.memory.keys())
202
319
  return [block.label for block in self.blocks]
203
320
 
204
- # TODO: these should actually be label, not name
205
321
  def get_block(self, label: str) -> Block:
206
322
  """Correct way to index into the memory.memory field, returns a Block"""
207
323
  keys = []
@@ -213,7 +329,6 @@ class Memory(BaseModel, validate_assignment=True):
213
329
 
214
330
  def get_blocks(self) -> List[Block]:
215
331
  """Return a list of the blocks held inside the memory object"""
216
- # return list(self.memory.values())
217
332
  return self.blocks
218
333
 
219
334
  def set_block(self, block: Block):
@@ -236,7 +351,6 @@ class Memory(BaseModel, validate_assignment=True):
236
351
  raise ValueError(f"Block with label {label} does not exist")
237
352
 
238
353
 
239
- # TODO: ideally this is refactored into ChatMemory and the subclasses are given more specific names.
240
354
  class BasicBlockMemory(Memory):
241
355
  """
242
356
  BasicBlockMemory is a basic implemention of the Memory class, which takes in a list of blocks and links them to the memory object. These are editable by the agent via the core memory functions.
@@ -308,7 +422,6 @@ class ChatMemory(BasicBlockMemory):
308
422
  human (str): The starter value for the human block.
309
423
  limit (int): The character limit for each block.
310
424
  """
311
- # TODO: Should these be CreateBlocks?
312
425
  super().__init__(blocks=[Block(value=persona, limit=limit, label="persona"), Block(value=human, limit=limit, label="human")])
313
426
 
314
427
 
@@ -19,7 +19,6 @@ class ProviderTraceCreate(BaseModel):
19
19
  request_json: dict[str, Any] = Field(..., description="JSON content of the provider request")
20
20
  response_json: dict[str, Any] = Field(..., description="JSON content of the provider response")
21
21
  step_id: str = Field(None, description="ID of the step that this trace is associated with")
22
- organization_id: str = Field(..., description="The unique identifier of the organization.")
23
22
 
24
23
 
25
24
  class ProviderTrace(BaseProviderTrace):
@@ -39,5 +38,4 @@ class ProviderTrace(BaseProviderTrace):
39
38
  request_json: Dict[str, Any] = Field(..., description="JSON content of the provider request")
40
39
  response_json: Dict[str, Any] = Field(..., description="JSON content of the provider response")
41
40
  step_id: Optional[str] = Field(None, description="ID of the step that this trace is associated with")
42
- organization_id: str = Field(..., description="The unique identifier of the organization.")
43
41
  created_at: datetime = Field(default_factory=get_utc_time, description="The timestamp when the object was created.")
letta/schemas/run.py CHANGED
@@ -4,6 +4,7 @@ from pydantic import Field
4
4
 
5
5
  from letta.schemas.enums import JobType
6
6
  from letta.schemas.job import Job, JobBase, LettaRequestConfig
7
+ from letta.schemas.letta_stop_reason import StopReasonType
7
8
 
8
9
 
9
10
  class RunBase(JobBase):
@@ -29,6 +30,7 @@ class Run(RunBase):
29
30
  id: str = RunBase.generate_id_field()
30
31
  user_id: Optional[str] = Field(None, description="The unique identifier of the user associated with the run.")
31
32
  request_config: Optional[LettaRequestConfig] = Field(None, description="The request configuration for the run.")
33
+ stop_reason: Optional[StopReasonType] = Field(None, description="The reason why the run was stopped.")
32
34
 
33
35
  @classmethod
34
36
  def from_job(cls, job: Job) -> "Run":