letta-nightly 0.11.7.dev20251006104136__py3-none-any.whl → 0.11.7.dev20251008104128__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 (145) hide show
  1. letta/adapters/letta_llm_adapter.py +1 -0
  2. letta/adapters/letta_llm_request_adapter.py +0 -1
  3. letta/adapters/letta_llm_stream_adapter.py +7 -2
  4. letta/adapters/simple_llm_request_adapter.py +88 -0
  5. letta/adapters/simple_llm_stream_adapter.py +192 -0
  6. letta/agents/agent_loop.py +6 -0
  7. letta/agents/ephemeral_summary_agent.py +2 -1
  8. letta/agents/helpers.py +142 -6
  9. letta/agents/letta_agent.py +13 -33
  10. letta/agents/letta_agent_batch.py +2 -4
  11. letta/agents/letta_agent_v2.py +87 -77
  12. letta/agents/letta_agent_v3.py +899 -0
  13. letta/agents/voice_agent.py +2 -6
  14. letta/constants.py +8 -4
  15. letta/errors.py +40 -0
  16. letta/functions/function_sets/base.py +84 -4
  17. letta/functions/function_sets/multi_agent.py +0 -3
  18. letta/functions/schema_generator.py +113 -71
  19. letta/groups/dynamic_multi_agent.py +3 -2
  20. letta/groups/helpers.py +1 -2
  21. letta/groups/round_robin_multi_agent.py +3 -2
  22. letta/groups/sleeptime_multi_agent.py +3 -2
  23. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  24. letta/groups/sleeptime_multi_agent_v3.py +17 -17
  25. letta/groups/supervisor_multi_agent.py +84 -80
  26. letta/helpers/converters.py +3 -0
  27. letta/helpers/message_helper.py +4 -0
  28. letta/helpers/tool_rule_solver.py +92 -5
  29. letta/interfaces/anthropic_streaming_interface.py +409 -0
  30. letta/interfaces/gemini_streaming_interface.py +296 -0
  31. letta/interfaces/openai_streaming_interface.py +752 -1
  32. letta/llm_api/anthropic_client.py +126 -16
  33. letta/llm_api/bedrock_client.py +4 -2
  34. letta/llm_api/deepseek_client.py +4 -1
  35. letta/llm_api/google_vertex_client.py +123 -42
  36. letta/llm_api/groq_client.py +4 -1
  37. letta/llm_api/llm_api_tools.py +11 -4
  38. letta/llm_api/llm_client_base.py +6 -2
  39. letta/llm_api/openai.py +32 -2
  40. letta/llm_api/openai_client.py +423 -18
  41. letta/llm_api/xai_client.py +4 -1
  42. letta/main.py +9 -5
  43. letta/memory.py +1 -0
  44. letta/orm/__init__.py +1 -1
  45. letta/orm/agent.py +10 -0
  46. letta/orm/block.py +7 -16
  47. letta/orm/blocks_agents.py +8 -2
  48. letta/orm/files_agents.py +2 -0
  49. letta/orm/job.py +7 -5
  50. letta/orm/mcp_oauth.py +1 -0
  51. letta/orm/message.py +21 -6
  52. letta/orm/organization.py +2 -0
  53. letta/orm/provider.py +6 -2
  54. letta/orm/run.py +71 -0
  55. letta/orm/sandbox_config.py +7 -1
  56. letta/orm/sqlalchemy_base.py +0 -306
  57. letta/orm/step.py +6 -5
  58. letta/orm/step_metrics.py +5 -5
  59. letta/otel/tracing.py +28 -3
  60. letta/plugins/defaults.py +4 -4
  61. letta/prompts/system_prompts/__init__.py +2 -0
  62. letta/prompts/system_prompts/letta_v1.py +25 -0
  63. letta/schemas/agent.py +3 -2
  64. letta/schemas/agent_file.py +9 -3
  65. letta/schemas/block.py +23 -10
  66. letta/schemas/enums.py +21 -2
  67. letta/schemas/job.py +17 -4
  68. letta/schemas/letta_message_content.py +71 -2
  69. letta/schemas/letta_stop_reason.py +5 -5
  70. letta/schemas/llm_config.py +53 -3
  71. letta/schemas/memory.py +1 -1
  72. letta/schemas/message.py +504 -117
  73. letta/schemas/openai/responses_request.py +64 -0
  74. letta/schemas/providers/__init__.py +2 -0
  75. letta/schemas/providers/anthropic.py +16 -0
  76. letta/schemas/providers/ollama.py +115 -33
  77. letta/schemas/providers/openrouter.py +52 -0
  78. letta/schemas/providers/vllm.py +2 -1
  79. letta/schemas/run.py +48 -42
  80. letta/schemas/step.py +2 -2
  81. letta/schemas/step_metrics.py +1 -1
  82. letta/schemas/tool.py +15 -107
  83. letta/schemas/tool_rule.py +88 -5
  84. letta/serialize_schemas/marshmallow_agent.py +1 -0
  85. letta/server/db.py +86 -408
  86. letta/server/rest_api/app.py +61 -10
  87. letta/server/rest_api/dependencies.py +14 -0
  88. letta/server/rest_api/redis_stream_manager.py +19 -8
  89. letta/server/rest_api/routers/v1/agents.py +364 -292
  90. letta/server/rest_api/routers/v1/blocks.py +14 -20
  91. letta/server/rest_api/routers/v1/identities.py +45 -110
  92. letta/server/rest_api/routers/v1/internal_templates.py +21 -0
  93. letta/server/rest_api/routers/v1/jobs.py +23 -6
  94. letta/server/rest_api/routers/v1/messages.py +1 -1
  95. letta/server/rest_api/routers/v1/runs.py +126 -85
  96. letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
  97. letta/server/rest_api/routers/v1/tools.py +281 -594
  98. letta/server/rest_api/routers/v1/voice.py +1 -1
  99. letta/server/rest_api/streaming_response.py +29 -29
  100. letta/server/rest_api/utils.py +122 -64
  101. letta/server/server.py +160 -887
  102. letta/services/agent_manager.py +236 -919
  103. letta/services/agent_serialization_manager.py +16 -0
  104. letta/services/archive_manager.py +0 -100
  105. letta/services/block_manager.py +211 -168
  106. letta/services/file_manager.py +1 -1
  107. letta/services/files_agents_manager.py +24 -33
  108. letta/services/group_manager.py +0 -142
  109. letta/services/helpers/agent_manager_helper.py +7 -2
  110. letta/services/helpers/run_manager_helper.py +85 -0
  111. letta/services/job_manager.py +96 -411
  112. letta/services/lettuce/__init__.py +6 -0
  113. letta/services/lettuce/lettuce_client_base.py +86 -0
  114. letta/services/mcp_manager.py +38 -6
  115. letta/services/message_manager.py +165 -362
  116. letta/services/organization_manager.py +0 -36
  117. letta/services/passage_manager.py +0 -345
  118. letta/services/provider_manager.py +0 -80
  119. letta/services/run_manager.py +301 -0
  120. letta/services/sandbox_config_manager.py +0 -234
  121. letta/services/step_manager.py +62 -39
  122. letta/services/summarizer/summarizer.py +9 -7
  123. letta/services/telemetry_manager.py +0 -16
  124. letta/services/tool_executor/builtin_tool_executor.py +35 -0
  125. letta/services/tool_executor/core_tool_executor.py +397 -2
  126. letta/services/tool_executor/files_tool_executor.py +3 -3
  127. letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
  128. letta/services/tool_executor/tool_execution_manager.py +6 -8
  129. letta/services/tool_executor/tool_executor_base.py +3 -3
  130. letta/services/tool_manager.py +85 -339
  131. letta/services/tool_sandbox/base.py +24 -13
  132. letta/services/tool_sandbox/e2b_sandbox.py +16 -1
  133. letta/services/tool_schema_generator.py +123 -0
  134. letta/services/user_manager.py +0 -99
  135. letta/settings.py +20 -4
  136. {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/METADATA +3 -5
  137. {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/RECORD +140 -132
  138. letta/agents/temporal/activities/__init__.py +0 -4
  139. letta/agents/temporal/activities/example_activity.py +0 -7
  140. letta/agents/temporal/activities/prepare_messages.py +0 -10
  141. letta/agents/temporal/temporal_agent_workflow.py +0 -56
  142. letta/agents/temporal/types.py +0 -25
  143. {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/WHEEL +0 -0
  144. {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/entry_points.txt +0 -0
  145. {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/licenses/LICENSE +0 -0
@@ -10,6 +10,7 @@ from sqlalchemy.dialects.postgresql import insert as pg_insert
10
10
  from letta.constants import (
11
11
  BASE_MEMORY_TOOLS,
12
12
  BASE_MEMORY_TOOLS_V2,
13
+ BASE_MEMORY_TOOLS_V3,
13
14
  BASE_SLEEPTIME_CHAT_TOOLS,
14
15
  BASE_SLEEPTIME_TOOLS,
15
16
  BASE_TOOLS,
@@ -73,7 +74,7 @@ from letta.serialize_schemas.marshmallow_tool import SerializedToolSchema
73
74
  from letta.serialize_schemas.pydantic_agent_schema import AgentSchema
74
75
  from letta.server.db import db_registry
75
76
  from letta.services.archive_manager import ArchiveManager
76
- from letta.services.block_manager import BlockManager
77
+ from letta.services.block_manager import BlockManager, validate_block_limit_constraint
77
78
  from letta.services.context_window_calculator.context_window_calculator import ContextWindowCalculator
78
79
  from letta.services.context_window_calculator.token_counter import AnthropicTokenCounter, TiktokenCounter
79
80
  from letta.services.file_processor.chunker.line_chunker import LineChunker
@@ -301,185 +302,6 @@ class AgentManager:
301
302
  # ======================================================================================================================
302
303
  # Basic CRUD operations
303
304
  # ======================================================================================================================
304
- @trace_method
305
- def create_agent(self, agent_create: CreateAgent, actor: PydanticUser, _test_only_force_id: Optional[str] = None) -> PydanticAgentState:
306
- # validate required configs
307
- if not agent_create.llm_config or not agent_create.embedding_config:
308
- raise ValueError("llm_config and embedding_config are required")
309
-
310
- # blocks
311
- block_ids = list(agent_create.block_ids or [])
312
- if agent_create.memory_blocks:
313
- pydantic_blocks = [PydanticBlock(**b.model_dump(to_orm=True)) for b in agent_create.memory_blocks]
314
- created_blocks = self.block_manager.batch_create_blocks(
315
- pydantic_blocks,
316
- actor=actor,
317
- )
318
- block_ids.extend([blk.id for blk in created_blocks])
319
-
320
- # tools
321
- tool_names = set(agent_create.tools or [])
322
- if agent_create.include_base_tools:
323
- if agent_create.agent_type == AgentType.voice_sleeptime_agent:
324
- tool_names |= set(BASE_VOICE_SLEEPTIME_TOOLS)
325
- elif agent_create.agent_type == AgentType.voice_convo_agent:
326
- tool_names |= set(BASE_VOICE_SLEEPTIME_CHAT_TOOLS)
327
- elif agent_create.agent_type == AgentType.sleeptime_agent:
328
- tool_names |= set(BASE_SLEEPTIME_TOOLS)
329
- elif agent_create.enable_sleeptime:
330
- tool_names |= set(BASE_SLEEPTIME_CHAT_TOOLS)
331
- elif agent_create.agent_type == AgentType.memgpt_v2_agent:
332
- tool_names |= calculate_base_tools(is_v2=True)
333
- elif agent_create.agent_type == AgentType.react_agent:
334
- pass # no default tools
335
- elif agent_create.agent_type == AgentType.workflow_agent:
336
- pass # no default tools
337
- else:
338
- tool_names |= calculate_base_tools(is_v2=False)
339
- if agent_create.include_multi_agent_tools:
340
- tool_names |= calculate_multi_agent_tools()
341
-
342
- supplied_ids = set(agent_create.tool_ids or [])
343
-
344
- source_ids = agent_create.source_ids or []
345
- identity_ids = agent_create.identity_ids or []
346
- tag_values = agent_create.tags or []
347
-
348
- with db_registry.session() as session:
349
- with session.begin():
350
- name_to_id, id_to_name = self._resolve_tools(
351
- session,
352
- tool_names,
353
- supplied_ids,
354
- actor.organization_id,
355
- )
356
-
357
- tool_ids = set(name_to_id.values()) | set(id_to_name.keys())
358
- tool_names = set(name_to_id.keys()) # now canonical
359
-
360
- tool_rules = list(agent_create.tool_rules or [])
361
-
362
- # Override include_base_tool_rules to False if model matches exclusion keywords and include_base_tool_rules is not explicitly set to True
363
- if (
364
- (
365
- self._should_exclude_model_from_base_tool_rules(agent_create.llm_config.model)
366
- and agent_create.include_base_tool_rules is None
367
- )
368
- and agent_create.agent_type != AgentType.sleeptime_agent
369
- ) or agent_create.include_base_tool_rules is False:
370
- agent_create.include_base_tool_rules = False
371
- logger.info(f"Overriding include_base_tool_rules to False for model: {agent_create.llm_config.model}")
372
- else:
373
- agent_create.include_base_tool_rules = True
374
-
375
- should_add_base_tool_rules = agent_create.include_base_tool_rules
376
- if should_add_base_tool_rules:
377
- for tn in tool_names:
378
- if tn in {"send_message", "send_message_to_agent_async", "memory_finish_edits"}:
379
- tool_rules.append(TerminalToolRule(tool_name=tn))
380
- elif tn in (BASE_TOOLS + BASE_MEMORY_TOOLS + BASE_MEMORY_TOOLS_V2 + BASE_SLEEPTIME_TOOLS):
381
- tool_rules.append(ContinueToolRule(tool_name=tn))
382
-
383
- if tool_rules:
384
- check_supports_structured_output(model=agent_create.llm_config.model, tool_rules=tool_rules)
385
-
386
- new_agent = AgentModel(
387
- name=agent_create.name,
388
- system=derive_system_message(
389
- agent_type=agent_create.agent_type,
390
- enable_sleeptime=agent_create.enable_sleeptime,
391
- system=agent_create.system,
392
- ),
393
- hidden=agent_create.hidden,
394
- agent_type=agent_create.agent_type,
395
- llm_config=agent_create.llm_config,
396
- embedding_config=agent_create.embedding_config,
397
- organization_id=actor.organization_id,
398
- description=agent_create.description,
399
- metadata_=agent_create.metadata,
400
- tool_rules=tool_rules,
401
- project_id=agent_create.project_id,
402
- template_id=agent_create.template_id,
403
- base_template_id=agent_create.base_template_id,
404
- message_buffer_autoclear=agent_create.message_buffer_autoclear,
405
- enable_sleeptime=agent_create.enable_sleeptime,
406
- response_format=agent_create.response_format,
407
- created_by_id=actor.id,
408
- last_updated_by_id=actor.id,
409
- timezone=agent_create.timezone,
410
- max_files_open=agent_create.max_files_open,
411
- per_file_view_window_char_limit=agent_create.per_file_view_window_char_limit,
412
- )
413
-
414
- # Set template fields for InternalTemplateAgentCreate (similar to group creation)
415
- if isinstance(agent_create, InternalTemplateAgentCreate):
416
- new_agent.base_template_id = agent_create.base_template_id
417
- new_agent.template_id = agent_create.template_id
418
- new_agent.deployment_id = agent_create.deployment_id
419
- new_agent.entity_id = agent_create.entity_id
420
-
421
- if _test_only_force_id:
422
- new_agent.id = _test_only_force_id
423
-
424
- session.add(new_agent)
425
- session.flush()
426
- aid = new_agent.id
427
-
428
- # Note: These methods may need async versions if they perform database operations
429
- self._bulk_insert_pivot(
430
- session,
431
- ToolsAgents.__table__,
432
- [{"agent_id": aid, "tool_id": tid} for tid in tool_ids],
433
- )
434
-
435
- if block_ids:
436
- result = session.execute(select(BlockModel.id, BlockModel.label).where(BlockModel.id.in_(block_ids)))
437
- rows = [{"agent_id": aid, "block_id": bid, "block_label": lbl} for bid, lbl in result.all()]
438
- self._bulk_insert_pivot(session, BlocksAgents.__table__, rows)
439
-
440
- self._bulk_insert_pivot(
441
- session,
442
- SourcesAgents.__table__,
443
- [{"agent_id": aid, "source_id": sid} for sid in source_ids],
444
- )
445
- self._bulk_insert_pivot(
446
- session,
447
- AgentsTags.__table__,
448
- [{"agent_id": aid, "tag": tag} for tag in tag_values],
449
- )
450
- self._bulk_insert_pivot(
451
- session,
452
- IdentitiesAgents.__table__,
453
- [{"agent_id": aid, "identity_id": iid} for iid in identity_ids],
454
- )
455
-
456
- agent_secrets = agent_create.secrets or agent_create.tool_exec_environment_variables
457
- if agent_secrets:
458
- env_rows = [
459
- {
460
- "agent_id": aid,
461
- "key": key,
462
- "value": val,
463
- "organization_id": actor.organization_id,
464
- }
465
- for key, val in agent_secrets.items()
466
- ]
467
- session.execute(insert(AgentEnvironmentVariable).values(env_rows))
468
-
469
- # initial message sequence
470
- init_messages = self._generate_initial_message_sequence(
471
- actor,
472
- agent_state=new_agent.to_pydantic(include_relationships={"memory"}),
473
- supplied_initial_message_sequence=agent_create.initial_message_sequence,
474
- )
475
- new_agent.message_ids = [msg.id for msg in init_messages]
476
-
477
- session.refresh(new_agent)
478
-
479
- # Using the synchronous version since we don't have an async version yet
480
- # If you implement an async version of create_many_messages, you can switch to that
481
- self.message_manager.create_many_messages(pydantic_msgs=init_messages, actor=actor)
482
- return new_agent.to_pydantic()
483
305
 
484
306
  @trace_method
485
307
  async def create_agent_async(
@@ -493,8 +315,24 @@ class AgentManager:
493
315
  if not agent_create.llm_config or not agent_create.embedding_config:
494
316
  raise ValueError("llm_config and embedding_config are required")
495
317
 
496
- if agent_create.reasoning is not None:
497
- agent_create.llm_config = LLMConfig.apply_reasoning_setting_to_config(agent_create.llm_config, agent_create.reasoning)
318
+ # For v1 agents, enforce sane defaults even when reasoning is omitted
319
+ if agent_create.agent_type == AgentType.letta_v1_agent:
320
+ # Claude 3.7/4 or OpenAI o1/o3/o4/gpt-5
321
+ default_reasoning = LLMConfig.is_anthropic_reasoning_model(agent_create.llm_config) or LLMConfig.is_openai_reasoning_model(
322
+ agent_create.llm_config
323
+ )
324
+ agent_create.llm_config = LLMConfig.apply_reasoning_setting_to_config(
325
+ agent_create.llm_config,
326
+ agent_create.reasoning if agent_create.reasoning is not None else default_reasoning,
327
+ agent_create.agent_type,
328
+ )
329
+ else:
330
+ if agent_create.reasoning is not None:
331
+ agent_create.llm_config = LLMConfig.apply_reasoning_setting_to_config(
332
+ agent_create.llm_config,
333
+ agent_create.reasoning,
334
+ agent_create.agent_type,
335
+ )
498
336
 
499
337
  # blocks
500
338
  block_ids = list(agent_create.block_ids or [])
@@ -531,6 +369,15 @@ class AgentManager:
531
369
  tool_names |= calculate_base_tools(is_v2=True)
532
370
  elif agent_create.agent_type == AgentType.react_agent:
533
371
  pass # no default tools
372
+ elif agent_create.agent_type == AgentType.letta_v1_agent:
373
+ tool_names |= calculate_base_tools(is_v2=True)
374
+ # Remove `send_message` if it exists
375
+ tool_names.discard("send_message")
376
+ # NOTE: also overwriting inner_thoughts_in_kwargs to force False
377
+ agent_create.llm_config.put_inner_thoughts_in_kwargs = False
378
+ # NOTE: also overwrite initial message sequence to empty by default
379
+ if agent_create.initial_message_sequence is None:
380
+ agent_create.initial_message_sequence = []
534
381
  elif agent_create.agent_type == AgentType.workflow_agent:
535
382
  pass # no default tools
536
383
  else:
@@ -593,7 +440,7 @@ class AgentManager:
593
440
  for tn in tool_names:
594
441
  if tn in {"send_message", "send_message_to_agent_async", "memory_finish_edits"}:
595
442
  tool_rules.append(TerminalToolRule(tool_name=tn))
596
- elif tn in (BASE_TOOLS + BASE_MEMORY_TOOLS + BASE_MEMORY_TOOLS_V2 + BASE_SLEEPTIME_TOOLS):
443
+ elif tn in (BASE_TOOLS + BASE_MEMORY_TOOLS + BASE_MEMORY_TOOLS_V2 + BASE_MEMORY_TOOLS_V3 + BASE_SLEEPTIME_TOOLS):
597
444
  tool_rules.append(ContinueToolRule(tool_name=tn))
598
445
 
599
446
  for tool_with_requires_approval in requires_approval:
@@ -783,14 +630,6 @@ class AgentManager:
783
630
 
784
631
  return init_messages
785
632
 
786
- @enforce_types
787
- @trace_method
788
- def append_initial_message_sequence_to_in_context_messages(
789
- self, actor: PydanticUser, agent_state: PydanticAgentState, initial_message_sequence: Optional[List[MessageCreate]] = None
790
- ) -> PydanticAgentState:
791
- init_messages = self._generate_initial_message_sequence(actor, agent_state, initial_message_sequence)
792
- return self.append_to_in_context_messages(init_messages, agent_id=agent_state.id, actor=actor)
793
-
794
633
  @enforce_types
795
634
  @trace_method
796
635
  async def append_initial_message_sequence_to_in_context_messages_async(
@@ -799,130 +638,6 @@ class AgentManager:
799
638
  init_messages = await self._generate_initial_message_sequence_async(actor, agent_state, initial_message_sequence)
800
639
  return await self.append_to_in_context_messages_async(init_messages, agent_id=agent_state.id, actor=actor)
801
640
 
802
- @enforce_types
803
- @trace_method
804
- def update_agent(
805
- self,
806
- agent_id: str,
807
- agent_update: UpdateAgent,
808
- actor: PydanticUser,
809
- ) -> PydanticAgentState:
810
- new_tools = set(agent_update.tool_ids or [])
811
- new_sources = set(agent_update.source_ids or [])
812
- new_blocks = set(agent_update.block_ids or [])
813
- new_idents = set(agent_update.identity_ids or [])
814
- new_tags = set(agent_update.tags or [])
815
-
816
- with db_registry.session() as session, session.begin():
817
- agent: AgentModel = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
818
- agent.updated_at = datetime.now(timezone.utc)
819
- agent.last_updated_by_id = actor.id
820
-
821
- scalar_updates = {
822
- "name": agent_update.name,
823
- "system": agent_update.system,
824
- "llm_config": agent_update.llm_config,
825
- "embedding_config": agent_update.embedding_config,
826
- "message_ids": agent_update.message_ids,
827
- "tool_rules": agent_update.tool_rules,
828
- "description": agent_update.description,
829
- "project_id": agent_update.project_id,
830
- "template_id": agent_update.template_id,
831
- "base_template_id": agent_update.base_template_id,
832
- "message_buffer_autoclear": agent_update.message_buffer_autoclear,
833
- "enable_sleeptime": agent_update.enable_sleeptime,
834
- "response_format": agent_update.response_format,
835
- "last_run_completion": agent_update.last_run_completion,
836
- "last_run_duration_ms": agent_update.last_run_duration_ms,
837
- "max_files_open": agent_update.max_files_open,
838
- "per_file_view_window_char_limit": agent_update.per_file_view_window_char_limit,
839
- "timezone": agent_update.timezone,
840
- }
841
- for col, val in scalar_updates.items():
842
- if val is not None:
843
- setattr(agent, col, val)
844
-
845
- if agent_update.metadata is not None:
846
- agent.metadata_ = agent_update.metadata
847
-
848
- aid = agent.id
849
-
850
- if agent_update.tool_ids is not None:
851
- self._replace_pivot_rows(
852
- session,
853
- ToolsAgents.__table__,
854
- aid,
855
- [{"agent_id": aid, "tool_id": tid} for tid in new_tools],
856
- )
857
- session.expire(agent, ["tools"])
858
-
859
- if agent_update.source_ids is not None:
860
- self._replace_pivot_rows(
861
- session,
862
- SourcesAgents.__table__,
863
- aid,
864
- [{"agent_id": aid, "source_id": sid} for sid in new_sources],
865
- )
866
- session.expire(agent, ["sources"])
867
-
868
- if agent_update.block_ids is not None:
869
- rows = []
870
- if new_blocks:
871
- label_map = {
872
- bid: lbl
873
- for bid, lbl in session.execute(select(BlockModel.id, BlockModel.label).where(BlockModel.id.in_(new_blocks)))
874
- }
875
- rows = [{"agent_id": aid, "block_id": bid, "block_label": label_map[bid]} for bid in new_blocks]
876
-
877
- self._replace_pivot_rows(session, BlocksAgents.__table__, aid, rows)
878
- session.expire(agent, ["core_memory"])
879
-
880
- if agent_update.identity_ids is not None:
881
- self._replace_pivot_rows(
882
- session,
883
- IdentitiesAgents.__table__,
884
- aid,
885
- [{"agent_id": aid, "identity_id": iid} for iid in new_idents],
886
- )
887
- session.expire(agent, ["identities"])
888
-
889
- if agent_update.tags is not None:
890
- self._replace_pivot_rows(
891
- session,
892
- AgentsTags.__table__,
893
- aid,
894
- [{"agent_id": aid, "tag": tag} for tag in new_tags],
895
- )
896
- session.expire(agent, ["tags"])
897
-
898
- agent_secrets = agent_update.secrets or agent_update.tool_exec_environment_variables
899
- if agent_secrets is not None:
900
- session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
901
- env_rows = [
902
- {
903
- "agent_id": aid,
904
- "key": k,
905
- "value": v,
906
- "organization_id": agent.organization_id,
907
- }
908
- for k, v in agent_secrets.items()
909
- ]
910
- if env_rows:
911
- self._bulk_insert_pivot(session, AgentEnvironmentVariable.__table__, env_rows)
912
- session.expire(agent, ["tool_exec_environment_variables"])
913
-
914
- if agent_update.enable_sleeptime and agent_update.system is None:
915
- agent.system = derive_system_message(
916
- agent_type=agent.agent_type,
917
- enable_sleeptime=agent_update.enable_sleeptime,
918
- system=agent.system,
919
- )
920
-
921
- session.flush()
922
- session.refresh(agent)
923
-
924
- return agent.to_pydantic()
925
-
926
641
  @enforce_types
927
642
  @trace_method
928
643
  async def update_agent_async(
@@ -944,7 +659,11 @@ class AgentManager:
944
659
 
945
660
  if agent_update.reasoning is not None:
946
661
  llm_config = agent_update.llm_config or agent.llm_config
947
- agent_update.llm_config = LLMConfig.apply_reasoning_setting_to_config(llm_config, agent_update.reasoning)
662
+ agent_update.llm_config = LLMConfig.apply_reasoning_setting_to_config(
663
+ llm_config,
664
+ agent_update.reasoning,
665
+ agent.agent_type,
666
+ )
948
667
 
949
668
  scalar_updates = {
950
669
  "name": agent_update.name,
@@ -1073,67 +792,6 @@ class AgentManager:
1073
792
  await agent.update_async(db_session=session, actor=actor, no_commit=True, no_refresh=True)
1074
793
  await session.commit()
1075
794
 
1076
- # TODO: Make this general and think about how to roll this into sqlalchemybase
1077
- @trace_method
1078
- def list_agents(
1079
- self,
1080
- actor: PydanticUser,
1081
- name: Optional[str] = None,
1082
- tags: Optional[List[str]] = None,
1083
- match_all_tags: bool = False,
1084
- before: Optional[str] = None,
1085
- after: Optional[str] = None,
1086
- limit: Optional[int] = 50,
1087
- query_text: Optional[str] = None,
1088
- project_id: Optional[str] = None,
1089
- template_id: Optional[str] = None,
1090
- base_template_id: Optional[str] = None,
1091
- identity_id: Optional[str] = None,
1092
- identifier_keys: Optional[List[str]] = None,
1093
- include_relationships: Optional[List[str]] = None,
1094
- ascending: bool = True,
1095
- sort_by: Optional[str] = "created_at",
1096
- ) -> List[PydanticAgentState]:
1097
- """
1098
- Retrieves agents with optimized filtering and optional field selection.
1099
-
1100
- Args:
1101
- actor: The User requesting the list
1102
- name (Optional[str]): Filter by agent name.
1103
- tags (Optional[List[str]]): Filter agents by tags.
1104
- match_all_tags (bool): If True, only return agents that match ALL given tags.
1105
- before (Optional[str]): Cursor for pagination.
1106
- after (Optional[str]): Cursor for pagination.
1107
- limit (Optional[int]): Maximum number of agents to return.
1108
- query_text (Optional[str]): Search agents by name.
1109
- project_id (Optional[str]): Filter by project ID.
1110
- template_id (Optional[str]): Filter by template ID.
1111
- base_template_id (Optional[str]): Filter by base template ID.
1112
- identity_id (Optional[str]): Filter by identifier ID.
1113
- identifier_keys (Optional[List[str]]): Search agents by identifier keys.
1114
- include_relationships (Optional[List[str]]): List of fields to load for performance optimization.
1115
- ascending
1116
-
1117
- Returns:
1118
- List[PydanticAgentState]: The filtered list of matching agents.
1119
- """
1120
- with db_registry.session() as session:
1121
- query = select(AgentModel).distinct(AgentModel.created_at, AgentModel.id)
1122
- query = AgentModel.apply_access_predicate(query, actor, ["read"], AccessType.ORGANIZATION)
1123
-
1124
- # Apply filters
1125
- query = _apply_filters(query, name, query_text, project_id, template_id, base_template_id)
1126
- query = _apply_identity_filters(query, identity_id, identifier_keys)
1127
- query = _apply_tag_filter(query, tags, match_all_tags)
1128
- query = _apply_pagination(query, before, after, session, ascending=ascending, sort_by=sort_by)
1129
-
1130
- if limit:
1131
- query = query.limit(limit)
1132
-
1133
- result = session.execute(query)
1134
- agents = result.scalars().all()
1135
- return [agent.to_pydantic(include_relationships=include_relationships) for agent in agents]
1136
-
1137
795
  @trace_method
1138
796
  async def list_agents_async(
1139
797
  self,
@@ -1201,50 +859,6 @@ class AgentManager:
1201
859
  agents = result.scalars().all()
1202
860
  return await asyncio.gather(*[agent.to_pydantic_async(include_relationships=include_relationships) for agent in agents])
1203
861
 
1204
- @enforce_types
1205
- @trace_method
1206
- def list_agents_matching_tags(
1207
- self,
1208
- actor: PydanticUser,
1209
- match_all: List[str],
1210
- match_some: List[str],
1211
- limit: Optional[int] = 50,
1212
- ) -> List[PydanticAgentState]:
1213
- """
1214
- Retrieves agents in the same organization that match all specified `match_all` tags
1215
- and at least one tag from `match_some`. The query is optimized for efficiency by
1216
- leveraging indexed filtering and aggregation.
1217
-
1218
- Args:
1219
- actor (PydanticUser): The user requesting the agent list.
1220
- match_all (List[str]): Agents must have all these tags.
1221
- match_some (List[str]): Agents must have at least one of these tags.
1222
- limit (Optional[int]): Maximum number of agents to return.
1223
-
1224
- Returns:
1225
- List[PydanticAgentState: The filtered list of matching agents.
1226
- """
1227
- with db_registry.session() as session:
1228
- query = select(AgentModel).where(AgentModel.organization_id == actor.organization_id)
1229
-
1230
- if match_all:
1231
- # Subquery to find agent IDs that contain all match_all tags
1232
- subquery = (
1233
- select(AgentsTags.agent_id)
1234
- .where(AgentsTags.tag.in_(match_all))
1235
- .group_by(AgentsTags.agent_id)
1236
- .having(func.count(AgentsTags.tag) == literal(len(match_all)))
1237
- )
1238
- query = query.where(AgentModel.id.in_(subquery))
1239
-
1240
- if match_some:
1241
- # Ensures agents match at least one tag in match_some
1242
- query = query.join(AgentsTags).where(AgentsTags.tag.in_(match_some))
1243
-
1244
- query = query.distinct(AgentModel.id).order_by(AgentModel.id).limit(limit)
1245
-
1246
- return list(session.execute(query).scalars())
1247
-
1248
862
  @enforce_types
1249
863
  @trace_method
1250
864
  async def list_agents_matching_tags_async(
@@ -1289,17 +903,6 @@ class AgentManager:
1289
903
  result = await session.execute(query)
1290
904
  return await asyncio.gather(*[agent.to_pydantic_async() for agent in result.scalars()])
1291
905
 
1292
- @trace_method
1293
- def size(
1294
- self,
1295
- actor: PydanticUser,
1296
- ) -> int:
1297
- """
1298
- Get the total count of agents for the given user.
1299
- """
1300
- with db_registry.session() as session:
1301
- return AgentModel.size(db_session=session, actor=actor)
1302
-
1303
906
  @trace_method
1304
907
  async def size_async(
1305
908
  self,
@@ -1311,14 +914,6 @@ class AgentManager:
1311
914
  async with db_registry.async_session() as session:
1312
915
  return await AgentModel.size_async(db_session=session, actor=actor)
1313
916
 
1314
- @enforce_types
1315
- @trace_method
1316
- def get_agent_by_id(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
1317
- """Fetch an agent by its ID."""
1318
- with db_registry.session() as session:
1319
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
1320
- return agent.to_pydantic()
1321
-
1322
917
  @enforce_types
1323
918
  @trace_method
1324
919
  async def get_agent_by_id_async(
@@ -1342,6 +937,9 @@ class AgentManager:
1342
937
  raise NoResultFound(f"Agent with ID {agent_id} not found")
1343
938
 
1344
939
  return await agent.to_pydantic_async(include_relationships=include_relationships)
940
+ except NoResultFound:
941
+ # Re-raise NoResultFound without logging to preserve 404 handling
942
+ raise
1345
943
  except Exception as e:
1346
944
  logger.error(f"Error fetching agent {agent_id}: {str(e)}")
1347
945
  raise
@@ -1374,14 +972,6 @@ class AgentManager:
1374
972
  logger.error(f"Error fetching agents with IDs {agent_ids}: {str(e)}")
1375
973
  raise
1376
974
 
1377
- @enforce_types
1378
- @trace_method
1379
- def get_agent_by_name(self, agent_name: str, actor: PydanticUser) -> PydanticAgentState:
1380
- """Fetch an agent by its ID."""
1381
- with db_registry.session() as session:
1382
- agent = AgentModel.read(db_session=session, name=agent_name, actor=actor)
1383
- return agent.to_pydantic()
1384
-
1385
975
  @enforce_types
1386
976
  @trace_method
1387
977
  async def get_agent_archive_ids_async(self, agent_id: str, actor: PydanticUser) -> List[str]:
@@ -1395,54 +985,6 @@ class AgentManager:
1395
985
  archive_ids = [row[0] for row in result.fetchall()]
1396
986
  return archive_ids
1397
987
 
1398
- @enforce_types
1399
- @trace_method
1400
- def delete_agent(self, agent_id: str, actor: PydanticUser) -> None:
1401
- """
1402
- Deletes an agent and its associated relationships.
1403
- Ensures proper permission checks and cascades where applicable.
1404
-
1405
- Args:
1406
- agent_id: ID of the agent to be deleted.
1407
- actor: User performing the action.
1408
-
1409
- Raises:
1410
- NoResultFound: If agent doesn't exist
1411
- """
1412
- with db_registry.session() as session:
1413
- # Retrieve the agent
1414
- logger.debug(f"Hard deleting Agent with ID: {agent_id} with actor={actor}")
1415
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
1416
- agents_to_delete = [agent]
1417
- sleeptime_group_to_delete = None
1418
-
1419
- # Delete sleeptime agent and group (TODO this is flimsy pls fix)
1420
- if agent.multi_agent_group:
1421
- participant_agent_ids = agent.multi_agent_group.agent_ids
1422
- if agent.multi_agent_group.manager_type in {ManagerType.sleeptime, ManagerType.voice_sleeptime} and participant_agent_ids:
1423
- for participant_agent_id in participant_agent_ids:
1424
- try:
1425
- sleeptime_agent = AgentModel.read(db_session=session, identifier=participant_agent_id, actor=actor)
1426
- agents_to_delete.append(sleeptime_agent)
1427
- except NoResultFound:
1428
- pass # agent already deleted
1429
- sleeptime_agent_group = GroupModel.read(db_session=session, identifier=agent.multi_agent_group.id, actor=actor)
1430
- sleeptime_group_to_delete = sleeptime_agent_group
1431
-
1432
- try:
1433
- if sleeptime_group_to_delete is not None:
1434
- session.delete(sleeptime_group_to_delete)
1435
- session.commit()
1436
- for agent in agents_to_delete:
1437
- session.delete(agent)
1438
- session.commit()
1439
- except Exception as e:
1440
- session.rollback()
1441
- logger.exception(f"Failed to hard delete Agent with ID {agent_id}")
1442
- raise ValueError(f"Failed to hard delete Agent with ID {agent_id}: {e}")
1443
- else:
1444
- logger.debug(f"Agent with ID {agent_id} successfully hard deleted")
1445
-
1446
988
  @enforce_types
1447
989
  @trace_method
1448
990
  async def delete_agent_async(self, agent_id: str, actor: PydanticUser) -> None:
@@ -1493,168 +1035,9 @@ class AgentManager:
1493
1035
  else:
1494
1036
  logger.debug(f"Agent with ID {agent_id} successfully hard deleted")
1495
1037
 
1496
- @enforce_types
1497
- @trace_method
1498
- def serialize(self, agent_id: str, actor: PydanticUser, max_steps: Optional[int] = None) -> AgentSchema:
1499
- with db_registry.session() as session:
1500
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
1501
- schema = MarshmallowAgentSchema(session=session, actor=actor, max_steps=max_steps)
1502
- data = schema.dump(agent)
1503
- return AgentSchema(**data)
1504
-
1505
- @enforce_types
1506
- @trace_method
1507
- def deserialize(
1508
- self,
1509
- serialized_agent: AgentSchema,
1510
- actor: PydanticUser,
1511
- append_copy_suffix: bool = True,
1512
- override_existing_tools: bool = True,
1513
- project_id: Optional[str] = None,
1514
- strip_messages: Optional[bool] = False,
1515
- env_vars: Optional[dict[str, Any]] = None,
1516
- ) -> PydanticAgentState:
1517
- serialized_agent_dict = serialized_agent.model_dump()
1518
- tool_data_list = serialized_agent_dict.pop("tools", [])
1519
- messages = serialized_agent_dict.pop(MarshmallowAgentSchema.FIELD_MESSAGES, [])
1520
-
1521
- for msg in messages:
1522
- msg[MarshmallowAgentSchema.FIELD_ID] = SerializedMessageSchema.generate_id() # Generate new ID
1523
-
1524
- message_ids = []
1525
- in_context_message_indices = serialized_agent_dict.pop(MarshmallowAgentSchema.FIELD_IN_CONTEXT_INDICES)
1526
- for idx in in_context_message_indices:
1527
- message_ids.append(messages[idx][MarshmallowAgentSchema.FIELD_ID])
1528
-
1529
- serialized_agent_dict[MarshmallowAgentSchema.FIELD_MESSAGE_IDS] = message_ids
1530
-
1531
- with db_registry.session() as session:
1532
- schema = MarshmallowAgentSchema(session=session, actor=actor)
1533
- agent = schema.load(serialized_agent_dict, session=session)
1534
-
1535
- agent.organization_id = actor.organization_id
1536
- for block in agent.core_memory:
1537
- block.organization_id = actor.organization_id
1538
- if append_copy_suffix:
1539
- agent.name += "_copy"
1540
- if project_id:
1541
- agent.project_id = project_id
1542
-
1543
- if strip_messages:
1544
- # we want to strip all but the first (system) message
1545
- agent.message_ids = [agent.message_ids[0]]
1546
-
1547
- if env_vars:
1548
- for var in agent.tool_exec_environment_variables:
1549
- var.value = env_vars.get(var.key, "")
1550
- for var in agent.secrets:
1551
- var.value = env_vars.get(var.key, "")
1552
-
1553
- agent = agent.create(session, actor=actor)
1554
-
1555
- pydantic_agent = agent.to_pydantic()
1556
-
1557
- pyd_msgs = []
1558
- message_schema = SerializedMessageSchema(session=session, actor=actor)
1559
-
1560
- for serialized_message in messages:
1561
- pydantic_message = message_schema.load(serialized_message, session=session).to_pydantic()
1562
- pydantic_message.agent_id = agent.id
1563
- pyd_msgs.append(pydantic_message)
1564
- self.message_manager.create_many_messages(pyd_msgs, actor=actor)
1565
-
1566
- # Need to do this separately as there's some fancy upsert logic that SqlAlchemy cannot handle
1567
- for tool_data in tool_data_list:
1568
- pydantic_tool = SerializedToolSchema(actor=actor).load(tool_data, transient=True).to_pydantic()
1569
-
1570
- existing_pydantic_tool = self.tool_manager.get_tool_by_name(pydantic_tool.name, actor=actor)
1571
- if existing_pydantic_tool and (
1572
- existing_pydantic_tool.tool_type in {ToolType.LETTA_CORE, ToolType.LETTA_MULTI_AGENT_CORE, ToolType.LETTA_MEMORY_CORE}
1573
- or not override_existing_tools
1574
- ):
1575
- pydantic_tool = existing_pydantic_tool
1576
- else:
1577
- pydantic_tool = self.tool_manager.create_or_update_tool(pydantic_tool, actor=actor, bypass_name_check=True)
1578
-
1579
- pydantic_agent = self.attach_tool(agent_id=pydantic_agent.id, tool_id=pydantic_tool.id, actor=actor)
1580
-
1581
- return pydantic_agent
1582
-
1583
1038
  # ======================================================================================================================
1584
1039
  # Per Agent Environment Variable Management
1585
1040
  # ======================================================================================================================
1586
- @enforce_types
1587
- @trace_method
1588
- def _set_environment_variables(
1589
- self,
1590
- agent_id: str,
1591
- env_vars: Dict[str, str],
1592
- actor: PydanticUser,
1593
- ) -> PydanticAgentState:
1594
- """
1595
- Adds or replaces the environment variables for the specified agent.
1596
-
1597
- Args:
1598
- agent_id: The agent id.
1599
- env_vars: A dictionary of environment variable key-value pairs.
1600
- actor: The user performing the action.
1601
-
1602
- Returns:
1603
- PydanticAgentState: The updated agent as a Pydantic model.
1604
- """
1605
- with db_registry.session() as session:
1606
- # Retrieve the agent
1607
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
1608
-
1609
- # Fetch existing environment variables as a dictionary
1610
- existing_vars = {var.key: var for var in agent.tool_exec_environment_variables}
1611
-
1612
- # Update or create environment variables
1613
- updated_vars = []
1614
- for key, value in env_vars.items():
1615
- if key in existing_vars:
1616
- # Update existing variable
1617
- existing_vars[key].value = value
1618
- updated_vars.append(existing_vars[key])
1619
- else:
1620
- # Create new variable
1621
- updated_vars.append(
1622
- AgentEnvironmentVariableModel(
1623
- key=key,
1624
- value=value,
1625
- agent_id=agent_id,
1626
- organization_id=actor.organization_id,
1627
- created_by_id=actor.id,
1628
- last_updated_by_id=actor.id,
1629
- )
1630
- )
1631
-
1632
- # Remove stale variables
1633
- stale_keys = set(existing_vars) - set(env_vars)
1634
- agent.tool_exec_environment_variables = [var for var in updated_vars if var.key not in stale_keys]
1635
- agent.secrets = [var for var in updated_vars if var.key not in stale_keys]
1636
-
1637
- # Update the agent in the database
1638
- agent.update(session, actor=actor)
1639
-
1640
- # Return the updated agent state
1641
- return agent.to_pydantic()
1642
-
1643
- @enforce_types
1644
- @trace_method
1645
- def list_groups(self, agent_id: str, actor: PydanticUser, manager_type: Optional[str] = None) -> List[PydanticGroup]:
1646
- with db_registry.session() as session:
1647
- query = (
1648
- select(GroupModel)
1649
- .join(GroupsAgents, GroupModel.id == GroupsAgents.group_id)
1650
- .where(GroupsAgents.agent_id == agent_id, GroupModel.organization_id == actor.organization_id)
1651
- )
1652
-
1653
- if manager_type:
1654
- query = query.where(GroupModel.manager_type == manager_type)
1655
-
1656
- result = session.execute(query)
1657
- return [group.to_pydantic() for group in result.scalars()]
1658
1041
 
1659
1042
  # ======================================================================================================================
1660
1043
  # In Context Messages Management
@@ -1666,9 +1049,9 @@ class AgentManager:
1666
1049
  # TODO: This can also be made more efficient, instead of getting, setting, we can do it all in one db session for one query.
1667
1050
  @enforce_types
1668
1051
  @trace_method
1669
- def get_in_context_messages(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
1670
- message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
1671
- return self.message_manager.get_messages_by_ids(message_ids=message_ids, actor=actor)
1052
+ async def get_in_context_messages(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
1053
+ agent_state = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor)
1054
+ return await self.message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor)
1672
1055
 
1673
1056
  @enforce_types
1674
1057
  @trace_method
@@ -1924,10 +1307,8 @@ class AgentManager:
1924
1307
  PydanticAgentState: The updated agent state with only the original system message preserved.
1925
1308
  """
1926
1309
  async with db_registry.async_session() as session:
1927
- # Retrieve the existing agent (will raise NoResultFound if invalid)
1928
1310
  agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
1929
1311
 
1930
- # Ensure agent has message_ids with at least one message
1931
1312
  if not agent.message_ids or len(agent.message_ids) == 0:
1932
1313
  logger.error(
1933
1314
  f"Agent {agent_id} has no message_ids. Agent details: "
@@ -1936,13 +1317,12 @@ class AgentManager:
1936
1317
  )
1937
1318
  raise ValueError(f"Agent {agent_id} has no message_ids - cannot preserve system message")
1938
1319
 
1939
- # Get the system message ID (first message)
1940
1320
  system_message_id = agent.message_ids[0]
1941
1321
 
1942
- # Delete all messages for the agent except the system message
1943
- await self.message_manager.delete_all_messages_for_agent_async(agent_id=agent_id, actor=actor, exclude_ids=[system_message_id])
1322
+ await self.message_manager.delete_all_messages_for_agent_async(agent_id=agent_id, actor=actor, exclude_ids=[system_message_id])
1944
1323
 
1945
- # Update agent to only keep the system message
1324
+ async with db_registry.async_session() as session:
1325
+ agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
1946
1326
  agent.message_ids = [system_message_id]
1947
1327
  await agent.update_async(db_session=session, actor=actor)
1948
1328
  agent_state = await agent.to_pydantic_async(include_relationships=["sources"])
@@ -1989,19 +1369,22 @@ class AgentManager:
1989
1369
  )
1990
1370
  if new_memory_str not in system_message.content[0].text:
1991
1371
  # update the blocks (LRW) in the DB
1992
- for label in agent_state.memory.list_block_labels():
1993
- updated_value = new_memory.get_block(label).value
1994
- if updated_value != agent_state.memory.get_block(label).value:
1995
- # update the block if it's changed
1996
- block_id = agent_state.memory.get_block(label).id
1997
- await self.block_manager.update_block_async(
1998
- block_id=block_id, block_update=BlockUpdate(value=updated_value), actor=actor
1999
- )
1372
+ for label in new_memory.list_block_labels():
1373
+ if label in agent_state.memory.list_block_labels():
1374
+ # Block exists in both old and new memory - check if value changed
1375
+ updated_value = new_memory.get_block(label).value
1376
+ if updated_value != agent_state.memory.get_block(label).value:
1377
+ # update the block if it's changed
1378
+ block_id = agent_state.memory.get_block(label).id
1379
+ await self.block_manager.update_block_async(
1380
+ block_id=block_id, block_update=BlockUpdate(value=updated_value), actor=actor
1381
+ )
2000
1382
 
2001
- # refresh memory from DB (using block ids)
2002
- blocks = await self.block_manager.get_all_blocks_by_ids_async(
2003
- block_ids=[b.id for b in agent_state.memory.get_blocks()], actor=actor
2004
- )
1383
+ # Note: New blocks are already persisted in the creation methods,
1384
+ # so we don't need to handle them here
1385
+
1386
+ # refresh memory from DB (using block ids from the new memory)
1387
+ blocks = await self.block_manager.get_all_blocks_by_ids_async(block_ids=[b.id for b in new_memory.get_blocks()], actor=actor)
2005
1388
 
2006
1389
  agent_state.memory = Memory(
2007
1390
  blocks=blocks,
@@ -2166,16 +1549,28 @@ class AgentManager:
2166
1549
 
2167
1550
  @enforce_types
2168
1551
  @trace_method
2169
- async def list_attached_sources_async(self, agent_id: str, actor: PydanticUser) -> List[PydanticSource]:
1552
+ async def list_attached_sources_async(
1553
+ self,
1554
+ agent_id: str,
1555
+ actor: PydanticUser,
1556
+ before: Optional[str] = None,
1557
+ after: Optional[str] = None,
1558
+ limit: Optional[int] = None,
1559
+ ascending: bool = False,
1560
+ ) -> List[PydanticSource]:
2170
1561
  """
2171
- Lists all sources attached to an agent.
1562
+ Lists all sources attached to an agent with pagination.
2172
1563
 
2173
1564
  Args:
2174
1565
  agent_id: ID of the agent to list sources for
2175
1566
  actor: User performing the action
1567
+ before: Source ID cursor for pagination. Returns sources that come before this source ID.
1568
+ after: Source ID cursor for pagination. Returns sources that come after this source ID.
1569
+ limit: Maximum number of sources to return.
1570
+ ascending: Sort order by creation time.
2176
1571
 
2177
1572
  Returns:
2178
- List[str]: List of source IDs attached to the agent
1573
+ List[PydanticSource]: List of sources attached to the agent
2179
1574
 
2180
1575
  Raises:
2181
1576
  NoResultFound: If agent doesn't exist or user doesn't have access
@@ -2194,9 +1589,24 @@ class AgentManager:
2194
1589
  SourceModel.organization_id == actor.organization_id,
2195
1590
  SourceModel.is_deleted == False,
2196
1591
  )
2197
- .order_by(SourceModel.created_at.desc(), SourceModel.id)
2198
1592
  )
2199
1593
 
1594
+ # Apply cursor-based pagination
1595
+ if before:
1596
+ query = query.where(SourceModel.id < before)
1597
+ if after:
1598
+ query = query.where(SourceModel.id > after)
1599
+
1600
+ # Apply sorting
1601
+ if ascending:
1602
+ query = query.order_by(SourceModel.created_at.asc(), SourceModel.id.asc())
1603
+ else:
1604
+ query = query.order_by(SourceModel.created_at.desc(), SourceModel.id.desc())
1605
+
1606
+ # Apply limit
1607
+ if limit:
1608
+ query = query.limit(limit)
1609
+
2200
1610
  result = await session.execute(query)
2201
1611
  sources = result.scalars().all()
2202
1612
 
@@ -2240,22 +1650,6 @@ class AgentManager:
2240
1650
  # ======================================================================================================================
2241
1651
  # Block management
2242
1652
  # ======================================================================================================================
2243
- @enforce_types
2244
- @trace_method
2245
- def get_block_with_label(
2246
- self,
2247
- agent_id: str,
2248
- block_label: str,
2249
- actor: PydanticUser,
2250
- ) -> PydanticBlock:
2251
- """Gets a block attached to an agent by its label."""
2252
- with db_registry.session() as session:
2253
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
2254
- for block in agent.core_memory:
2255
- if block.label == block_label:
2256
- return block.to_pydantic()
2257
- raise NoResultFound(f"No block with label '{block_label}' found for agent '{agent_id}'")
2258
-
2259
1653
  @enforce_types
2260
1654
  @trace_method
2261
1655
  async def get_block_with_label_async(
@@ -2294,67 +1688,15 @@ class AgentManager:
2294
1688
 
2295
1689
  update_data = block_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
2296
1690
 
1691
+ # Validate limit constraints before updating
1692
+ validate_block_limit_constraint(update_data, block)
1693
+
2297
1694
  for key, value in update_data.items():
2298
1695
  setattr(block, key, value)
2299
1696
 
2300
1697
  await block.update_async(session, actor=actor)
2301
1698
  return block.to_pydantic()
2302
1699
 
2303
- @enforce_types
2304
- @trace_method
2305
- def update_block_with_label(
2306
- self,
2307
- agent_id: str,
2308
- block_label: str,
2309
- new_block_id: str,
2310
- actor: PydanticUser,
2311
- ) -> PydanticAgentState:
2312
- """Updates which block is assigned to a specific label for an agent."""
2313
- with db_registry.session() as session:
2314
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
2315
- new_block = BlockModel.read(db_session=session, identifier=new_block_id, actor=actor)
2316
-
2317
- if new_block.label != block_label:
2318
- raise ValueError(f"New block label '{new_block.label}' doesn't match required label '{block_label}'")
2319
-
2320
- # Remove old block with this label if it exists
2321
- agent.core_memory = [b for b in agent.core_memory if b.label != block_label]
2322
-
2323
- # Add new block
2324
- agent.core_memory.append(new_block)
2325
- agent.update(session, actor=actor)
2326
- return agent.to_pydantic()
2327
-
2328
- @enforce_types
2329
- @trace_method
2330
- def attach_block(self, agent_id: str, block_id: str, actor: PydanticUser) -> PydanticAgentState:
2331
- """Attaches a block to an agent. For sleeptime agents, also attaches to paired agents in the same group."""
2332
- with db_registry.session() as session:
2333
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
2334
- block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)
2335
-
2336
- # Attach block to the main agent
2337
- agent.core_memory.append(block)
2338
- agent.update(session, actor=actor, no_commit=True)
2339
-
2340
- # If agent is part of a sleeptime group, attach block to the sleeptime_agent
2341
- if agent.multi_agent_group and agent.multi_agent_group.manager_type == ManagerType.sleeptime:
2342
- group = agent.multi_agent_group
2343
- # Find the sleeptime_agent in the group
2344
- for other_agent_id in group.agent_ids or []:
2345
- if other_agent_id != agent_id:
2346
- try:
2347
- other_agent = AgentModel.read(db_session=session, identifier=other_agent_id, actor=actor)
2348
- if other_agent.agent_type == AgentType.sleeptime_agent and block not in other_agent.core_memory:
2349
- other_agent.core_memory.append(block)
2350
- other_agent.update(session, actor=actor, no_commit=True)
2351
- except NoResultFound:
2352
- # Agent might not exist anymore, skip
2353
- continue
2354
- session.commit()
2355
-
2356
- return agent.to_pydantic()
2357
-
2358
1700
  @enforce_types
2359
1701
  @trace_method
2360
1702
  async def attach_block_async(self, agent_id: str, block_id: str, actor: PydanticUser) -> PydanticAgentState:
@@ -2391,27 +1733,6 @@ class AgentManager:
2391
1733
 
2392
1734
  return await agent.to_pydantic_async()
2393
1735
 
2394
- @enforce_types
2395
- @trace_method
2396
- def detach_block(
2397
- self,
2398
- agent_id: str,
2399
- block_id: str,
2400
- actor: PydanticUser,
2401
- ) -> PydanticAgentState:
2402
- """Detaches a block from an agent."""
2403
- with db_registry.session() as session:
2404
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
2405
- original_length = len(agent.core_memory)
2406
-
2407
- agent.core_memory = [b for b in agent.core_memory if b.id != block_id]
2408
-
2409
- if len(agent.core_memory) == original_length:
2410
- raise NoResultFound(f"No block with id '{block_id}' found for agent '{agent_id}' with actor id: '{actor.id}'")
2411
-
2412
- agent.update(session, actor=actor)
2413
- return agent.to_pydantic()
2414
-
2415
1736
  @enforce_types
2416
1737
  @trace_method
2417
1738
  async def detach_block_async(
@@ -2433,27 +1754,6 @@ class AgentManager:
2433
1754
  await agent.update_async(session, actor=actor)
2434
1755
  return await agent.to_pydantic_async()
2435
1756
 
2436
- @enforce_types
2437
- @trace_method
2438
- def detach_block_with_label(
2439
- self,
2440
- agent_id: str,
2441
- block_label: str,
2442
- actor: PydanticUser,
2443
- ) -> PydanticAgentState:
2444
- """Detaches a block with the specified label from an agent."""
2445
- with db_registry.session() as session:
2446
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
2447
- original_length = len(agent.core_memory)
2448
-
2449
- agent.core_memory = [b for b in agent.core_memory if b.label != block_label]
2450
-
2451
- if len(agent.core_memory) == original_length:
2452
- raise NoResultFound(f"No block with label '{block_label}' found for agent '{agent_id}' with actor id: '{actor.id}'")
2453
-
2454
- agent.update(session, actor=actor)
2455
- return agent.to_pydantic()
2456
-
2457
1757
  # ======================================================================================================================
2458
1758
  # Passage Management
2459
1759
  # ======================================================================================================================
@@ -2985,41 +2285,6 @@ class AgentManager:
2985
2285
  # Tool Management
2986
2286
  # ======================================================================================================================
2987
2287
  @enforce_types
2988
- @trace_method
2989
- def attach_tool(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
2990
- """
2991
- Attaches a tool to an agent.
2992
-
2993
- Args:
2994
- agent_id: ID of the agent to attach the tool to.
2995
- tool_id: ID of the tool to attach.
2996
- actor: User performing the action.
2997
-
2998
- Raises:
2999
- NoResultFound: If the agent or tool is not found.
3000
-
3001
- Returns:
3002
- PydanticAgentState: The updated agent state.
3003
- """
3004
- with db_registry.session() as session:
3005
- # Verify the agent exists and user has permission to access it
3006
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
3007
-
3008
- # Use the _process_relationship helper to attach the tool
3009
- _process_relationship(
3010
- session=session,
3011
- agent=agent,
3012
- relationship_name="tools",
3013
- model_class=ToolModel,
3014
- item_ids=[tool_id],
3015
- allow_partial=False, # Ensure the tool exists
3016
- replace=False, # Extend the existing tools
3017
- )
3018
-
3019
- # Commit and refresh the agent
3020
- agent.update(session, actor=actor)
3021
- return agent.to_pydantic()
3022
-
3023
2288
  @enforce_types
3024
2289
  @trace_method
3025
2290
  async def attach_tool_async(self, agent_id: str, tool_id: str, actor: PydanticUser) -> None:
@@ -3253,40 +2518,6 @@ class AgentManager:
3253
2518
 
3254
2519
  return PydanticAgentState(**agent_state_dict)
3255
2520
 
3256
- @enforce_types
3257
- @trace_method
3258
- def detach_tool(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
3259
- """
3260
- Detaches a tool from an agent.
3261
-
3262
- Args:
3263
- agent_id: ID of the agent to detach the tool from.
3264
- tool_id: ID of the tool to detach.
3265
- actor: User performing the action.
3266
-
3267
- Raises:
3268
- NoResultFound: If the agent or tool is not found.
3269
-
3270
- Returns:
3271
- PydanticAgentState: The updated agent state.
3272
- """
3273
- with db_registry.session() as session:
3274
- # Verify the agent exists and user has permission to access it
3275
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
3276
-
3277
- # Filter out the tool to be detached
3278
- remaining_tools = [tool for tool in agent.tools if tool.id != tool_id]
3279
-
3280
- if len(remaining_tools) == len(agent.tools): # Tool ID was not in the relationship
3281
- logger.warning(f"Attempted to remove unattached tool id={tool_id} from agent id={agent_id} by actor={actor}")
3282
-
3283
- # Update the tools relationship
3284
- agent.tools = remaining_tools
3285
-
3286
- # Commit and refresh the agent
3287
- agent.update(session, actor=actor)
3288
- return agent.to_pydantic()
3289
-
3290
2521
  @enforce_types
3291
2522
  @trace_method
3292
2523
  async def detach_tool_async(self, agent_id: str, tool_id: str, actor: PydanticUser) -> None:
@@ -3381,24 +2612,15 @@ class AgentManager:
3381
2612
 
3382
2613
  @enforce_types
3383
2614
  @trace_method
3384
- def list_attached_tools(self, agent_id: str, actor: PydanticUser) -> List[PydanticTool]:
3385
- """
3386
- List all tools attached to an agent.
3387
-
3388
- Args:
3389
- agent_id: ID of the agent to list tools for.
3390
- actor: User performing the action.
3391
-
3392
- Returns:
3393
- List[PydanticTool]: List of tools attached to the agent.
3394
- """
3395
- with db_registry.session() as session:
3396
- agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
3397
- return [tool.to_pydantic() for tool in agent.tools]
3398
-
3399
- @enforce_types
3400
- @trace_method
3401
- async def list_attached_tools_async(self, agent_id: str, actor: PydanticUser) -> List[PydanticTool]:
2615
+ async def list_attached_tools_async(
2616
+ self,
2617
+ agent_id: str,
2618
+ actor: PydanticUser,
2619
+ before: Optional[str] = None,
2620
+ after: Optional[str] = None,
2621
+ limit: Optional[int] = None,
2622
+ ascending: bool = False,
2623
+ ) -> List[PydanticTool]:
3402
2624
  """
3403
2625
  List all tools attached to an agent (async version with optimized performance).
3404
2626
  Uses direct SQL queries to avoid SqlAlchemyBase overhead.
@@ -3406,6 +2628,10 @@ class AgentManager:
3406
2628
  Args:
3407
2629
  agent_id: ID of the agent to list tools for.
3408
2630
  actor: User performing the action.
2631
+ before: Tool ID cursor for pagination. Returns tools that come before this tool ID.
2632
+ after: Tool ID cursor for pagination. Returns tools that come after this tool ID.
2633
+ limit: Maximum number of tools to return.
2634
+ ascending: Sort order by creation time.
3409
2635
 
3410
2636
  Returns:
3411
2637
  List[PydanticTool]: List of tools attached to the agent.
@@ -3421,10 +2647,140 @@ class AgentManager:
3421
2647
  .where(ToolsAgents.agent_id == agent_id, ToolModel.organization_id == actor.organization_id)
3422
2648
  )
3423
2649
 
2650
+ # Apply cursor-based pagination
2651
+ if before:
2652
+ query = query.where(ToolModel.id < before)
2653
+ if after:
2654
+ query = query.where(ToolModel.id > after)
2655
+
2656
+ # Apply sorting
2657
+ if ascending:
2658
+ query = query.order_by(ToolModel.created_at.asc())
2659
+ else:
2660
+ query = query.order_by(ToolModel.created_at.desc())
2661
+
2662
+ # Apply limit
2663
+ if limit:
2664
+ query = query.limit(limit)
2665
+
3424
2666
  result = await session.execute(query)
3425
2667
  tools = result.scalars().all()
3426
2668
  return [tool.to_pydantic() for tool in tools]
3427
2669
 
2670
+ @enforce_types
2671
+ @trace_method
2672
+ async def list_agent_blocks_async(
2673
+ self,
2674
+ agent_id: str,
2675
+ actor: PydanticUser,
2676
+ before: Optional[str] = None,
2677
+ after: Optional[str] = None,
2678
+ limit: Optional[int] = None,
2679
+ ascending: bool = False,
2680
+ ) -> List[PydanticBlock]:
2681
+ """
2682
+ List all blocks for a specific agent with pagination.
2683
+
2684
+ Args:
2685
+ agent_id: ID of the agent to find blocks for.
2686
+ actor: User performing the action.
2687
+ before: Block ID cursor for pagination. Returns blocks that come before this block ID.
2688
+ after: Block ID cursor for pagination. Returns blocks that come after this block ID.
2689
+ limit: Maximum number of blocks to return.
2690
+ ascending: Sort order by creation time.
2691
+
2692
+ Returns:
2693
+ List[PydanticBlock]: List of blocks for the agent.
2694
+ """
2695
+ async with db_registry.async_session() as session:
2696
+ # First verify agent exists and user has access
2697
+ await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
2698
+
2699
+ # Build query to get blocks for this agent with pagination
2700
+ query = (
2701
+ select(BlockModel)
2702
+ .join(BlocksAgents, BlockModel.id == BlocksAgents.block_id)
2703
+ .where(BlocksAgents.agent_id == agent_id, BlockModel.organization_id == actor.organization_id)
2704
+ )
2705
+
2706
+ # Apply cursor-based pagination
2707
+ if before:
2708
+ query = query.where(BlockModel.id < before)
2709
+ if after:
2710
+ query = query.where(BlockModel.id > after)
2711
+
2712
+ # Apply sorting - use id instead of created_at for core memory blocks
2713
+ if ascending:
2714
+ query = query.order_by(BlockModel.id.asc())
2715
+ else:
2716
+ query = query.order_by(BlockModel.id.desc())
2717
+
2718
+ # Apply limit
2719
+ if limit:
2720
+ query = query.limit(limit)
2721
+
2722
+ result = await session.execute(query)
2723
+ blocks = result.scalars().all()
2724
+
2725
+ return [block.to_pydantic() for block in blocks]
2726
+
2727
+ @enforce_types
2728
+ @trace_method
2729
+ async def list_groups_async(
2730
+ self,
2731
+ agent_id: str,
2732
+ actor: PydanticUser,
2733
+ manager_type: Optional[str] = None,
2734
+ before: Optional[str] = None,
2735
+ after: Optional[str] = None,
2736
+ limit: Optional[int] = None,
2737
+ ascending: bool = False,
2738
+ ) -> List[PydanticGroup]:
2739
+ """
2740
+ List all groups that contain the specified agent.
2741
+
2742
+ Args:
2743
+ agent_id: ID of the agent to find groups for.
2744
+ actor: User performing the action.
2745
+ manager_type: Optional manager type to filter by.
2746
+ before: Group ID cursor for pagination. Returns groups that come before this group ID.
2747
+ after: Group ID cursor for pagination. Returns groups that come after this group ID.
2748
+ limit: Maximum number of groups to return.
2749
+ ascending: Sort order by creation time.
2750
+
2751
+ Returns:
2752
+ List[PydanticGroup]: List of groups containing the agent.
2753
+ """
2754
+ async with db_registry.async_session() as session:
2755
+ query = (
2756
+ select(GroupModel)
2757
+ .join(GroupsAgents, GroupModel.id == GroupsAgents.group_id)
2758
+ .where(GroupsAgents.agent_id == agent_id, GroupModel.organization_id == actor.organization_id)
2759
+ )
2760
+
2761
+ if manager_type:
2762
+ query = query.where(GroupModel.manager_type == manager_type)
2763
+
2764
+ # Apply cursor-based pagination
2765
+ if before:
2766
+ query = query.where(GroupModel.id < before)
2767
+ if after:
2768
+ query = query.where(GroupModel.id > after)
2769
+
2770
+ # Apply sorting
2771
+ if ascending:
2772
+ query = query.order_by(GroupModel.created_at.asc())
2773
+ else:
2774
+ query = query.order_by(GroupModel.created_at.desc())
2775
+
2776
+ # Apply limit
2777
+ if limit:
2778
+ query = query.limit(limit)
2779
+
2780
+ result = await session.execute(query)
2781
+ groups = result.scalars().all()
2782
+ return [group.to_pydantic() for group in groups]
2783
+
3428
2784
  # ======================================================================================================================
3429
2785
  # File Management
3430
2786
  # ======================================================================================================================
@@ -3505,45 +2861,6 @@ class AgentManager:
3505
2861
  # ======================================================================================================================
3506
2862
  # Tag Management
3507
2863
  # ======================================================================================================================
3508
- @enforce_types
3509
- @trace_method
3510
- def list_tags(
3511
- self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50, query_text: Optional[str] = None
3512
- ) -> List[str]:
3513
- """
3514
- Get all tags a user has created, ordered alphabetically.
3515
-
3516
- Args:
3517
- actor: User performing the action.
3518
- after: Cursor for forward pagination.
3519
- limit: Maximum number of tags to return.
3520
- query_text: Query text to filter tags by.
3521
-
3522
- Returns:
3523
- List[str]: List of all tags.
3524
- """
3525
- with db_registry.session() as session:
3526
- query = (
3527
- session.query(AgentsTags.tag)
3528
- .join(AgentModel, AgentModel.id == AgentsTags.agent_id)
3529
- .filter(AgentModel.organization_id == actor.organization_id)
3530
- .distinct()
3531
- )
3532
-
3533
- if query_text:
3534
- if settings.database_engine is DatabaseChoice.POSTGRES:
3535
- # PostgreSQL: Use ILIKE for case-insensitive search
3536
- query = query.filter(AgentsTags.tag.ilike(f"%{query_text}%"))
3537
- else:
3538
- # SQLite: Use LIKE with LOWER for case-insensitive search
3539
- query = query.filter(func.lower(AgentsTags.tag).like(func.lower(f"%{query_text}%")))
3540
-
3541
- if after:
3542
- query = query.filter(AgentsTags.tag > after)
3543
-
3544
- query = query.order_by(AgentsTags.tag).limit(limit)
3545
- results = [tag[0] for tag in query.all()]
3546
- return results
3547
2864
 
3548
2865
  @enforce_types
3549
2866
  @trace_method