letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.12.0.dev20251009104148__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 (151) hide show
  1. letta/__init__.py +1 -1
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/letta_llm_request_adapter.py +0 -1
  4. letta/adapters/letta_llm_stream_adapter.py +7 -2
  5. letta/adapters/simple_llm_request_adapter.py +88 -0
  6. letta/adapters/simple_llm_stream_adapter.py +192 -0
  7. letta/agents/agent_loop.py +6 -0
  8. letta/agents/ephemeral_summary_agent.py +2 -1
  9. letta/agents/helpers.py +142 -6
  10. letta/agents/letta_agent.py +13 -33
  11. letta/agents/letta_agent_batch.py +2 -4
  12. letta/agents/letta_agent_v2.py +87 -77
  13. letta/agents/letta_agent_v3.py +927 -0
  14. letta/agents/voice_agent.py +2 -6
  15. letta/constants.py +8 -4
  16. letta/database_utils.py +161 -0
  17. letta/errors.py +40 -0
  18. letta/functions/function_sets/base.py +84 -4
  19. letta/functions/function_sets/multi_agent.py +0 -3
  20. letta/functions/schema_generator.py +113 -71
  21. letta/groups/dynamic_multi_agent.py +3 -2
  22. letta/groups/helpers.py +1 -2
  23. letta/groups/round_robin_multi_agent.py +3 -2
  24. letta/groups/sleeptime_multi_agent.py +3 -2
  25. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  26. letta/groups/sleeptime_multi_agent_v3.py +17 -17
  27. letta/groups/supervisor_multi_agent.py +84 -80
  28. letta/helpers/converters.py +3 -0
  29. letta/helpers/message_helper.py +4 -0
  30. letta/helpers/tool_rule_solver.py +92 -5
  31. letta/interfaces/anthropic_streaming_interface.py +409 -0
  32. letta/interfaces/gemini_streaming_interface.py +296 -0
  33. letta/interfaces/openai_streaming_interface.py +752 -1
  34. letta/llm_api/anthropic_client.py +127 -16
  35. letta/llm_api/bedrock_client.py +4 -2
  36. letta/llm_api/deepseek_client.py +4 -1
  37. letta/llm_api/google_vertex_client.py +124 -42
  38. letta/llm_api/groq_client.py +4 -1
  39. letta/llm_api/llm_api_tools.py +11 -4
  40. letta/llm_api/llm_client_base.py +6 -2
  41. letta/llm_api/openai.py +32 -2
  42. letta/llm_api/openai_client.py +423 -18
  43. letta/llm_api/xai_client.py +4 -1
  44. letta/main.py +9 -5
  45. letta/memory.py +1 -0
  46. letta/orm/__init__.py +2 -1
  47. letta/orm/agent.py +10 -0
  48. letta/orm/block.py +7 -16
  49. letta/orm/blocks_agents.py +8 -2
  50. letta/orm/files_agents.py +2 -0
  51. letta/orm/job.py +7 -5
  52. letta/orm/mcp_oauth.py +1 -0
  53. letta/orm/message.py +21 -6
  54. letta/orm/organization.py +2 -0
  55. letta/orm/provider.py +6 -2
  56. letta/orm/run.py +71 -0
  57. letta/orm/run_metrics.py +82 -0
  58. letta/orm/sandbox_config.py +7 -1
  59. letta/orm/sqlalchemy_base.py +0 -306
  60. letta/orm/step.py +6 -5
  61. letta/orm/step_metrics.py +5 -5
  62. letta/otel/tracing.py +28 -3
  63. letta/plugins/defaults.py +4 -4
  64. letta/prompts/system_prompts/__init__.py +2 -0
  65. letta/prompts/system_prompts/letta_v1.py +25 -0
  66. letta/schemas/agent.py +3 -2
  67. letta/schemas/agent_file.py +9 -3
  68. letta/schemas/block.py +23 -10
  69. letta/schemas/enums.py +21 -2
  70. letta/schemas/job.py +17 -4
  71. letta/schemas/letta_message_content.py +71 -2
  72. letta/schemas/letta_stop_reason.py +5 -5
  73. letta/schemas/llm_config.py +53 -3
  74. letta/schemas/memory.py +1 -1
  75. letta/schemas/message.py +564 -117
  76. letta/schemas/openai/responses_request.py +64 -0
  77. letta/schemas/providers/__init__.py +2 -0
  78. letta/schemas/providers/anthropic.py +16 -0
  79. letta/schemas/providers/ollama.py +115 -33
  80. letta/schemas/providers/openrouter.py +52 -0
  81. letta/schemas/providers/vllm.py +2 -1
  82. letta/schemas/run.py +48 -42
  83. letta/schemas/run_metrics.py +21 -0
  84. letta/schemas/step.py +2 -2
  85. letta/schemas/step_metrics.py +1 -1
  86. letta/schemas/tool.py +15 -107
  87. letta/schemas/tool_rule.py +88 -5
  88. letta/serialize_schemas/marshmallow_agent.py +1 -0
  89. letta/server/db.py +79 -408
  90. letta/server/rest_api/app.py +61 -10
  91. letta/server/rest_api/dependencies.py +14 -0
  92. letta/server/rest_api/redis_stream_manager.py +19 -8
  93. letta/server/rest_api/routers/v1/agents.py +364 -292
  94. letta/server/rest_api/routers/v1/blocks.py +14 -20
  95. letta/server/rest_api/routers/v1/identities.py +45 -110
  96. letta/server/rest_api/routers/v1/internal_templates.py +21 -0
  97. letta/server/rest_api/routers/v1/jobs.py +23 -6
  98. letta/server/rest_api/routers/v1/messages.py +1 -1
  99. letta/server/rest_api/routers/v1/runs.py +149 -99
  100. letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
  101. letta/server/rest_api/routers/v1/tools.py +281 -594
  102. letta/server/rest_api/routers/v1/voice.py +1 -1
  103. letta/server/rest_api/streaming_response.py +29 -29
  104. letta/server/rest_api/utils.py +122 -64
  105. letta/server/server.py +160 -887
  106. letta/services/agent_manager.py +236 -919
  107. letta/services/agent_serialization_manager.py +16 -0
  108. letta/services/archive_manager.py +0 -100
  109. letta/services/block_manager.py +211 -168
  110. letta/services/context_window_calculator/token_counter.py +1 -1
  111. letta/services/file_manager.py +1 -1
  112. letta/services/files_agents_manager.py +24 -33
  113. letta/services/group_manager.py +0 -142
  114. letta/services/helpers/agent_manager_helper.py +7 -2
  115. letta/services/helpers/run_manager_helper.py +69 -0
  116. letta/services/job_manager.py +96 -411
  117. letta/services/lettuce/__init__.py +6 -0
  118. letta/services/lettuce/lettuce_client_base.py +86 -0
  119. letta/services/mcp_manager.py +38 -6
  120. letta/services/message_manager.py +165 -362
  121. letta/services/organization_manager.py +0 -36
  122. letta/services/passage_manager.py +0 -345
  123. letta/services/provider_manager.py +0 -80
  124. letta/services/run_manager.py +364 -0
  125. letta/services/sandbox_config_manager.py +0 -234
  126. letta/services/step_manager.py +62 -39
  127. letta/services/summarizer/summarizer.py +9 -7
  128. letta/services/telemetry_manager.py +0 -16
  129. letta/services/tool_executor/builtin_tool_executor.py +35 -0
  130. letta/services/tool_executor/core_tool_executor.py +397 -2
  131. letta/services/tool_executor/files_tool_executor.py +3 -3
  132. letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
  133. letta/services/tool_executor/tool_execution_manager.py +6 -8
  134. letta/services/tool_executor/tool_executor_base.py +3 -3
  135. letta/services/tool_manager.py +85 -339
  136. letta/services/tool_sandbox/base.py +24 -13
  137. letta/services/tool_sandbox/e2b_sandbox.py +16 -1
  138. letta/services/tool_schema_generator.py +123 -0
  139. letta/services/user_manager.py +0 -99
  140. letta/settings.py +20 -4
  141. letta/system.py +5 -1
  142. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
  143. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
  144. letta/agents/temporal/activities/__init__.py +0 -4
  145. letta/agents/temporal/activities/example_activity.py +0 -7
  146. letta/agents/temporal/activities/prepare_messages.py +0 -10
  147. letta/agents/temporal/temporal_agent_workflow.py +0 -56
  148. letta/agents/temporal/types.py +0 -25
  149. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
  150. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
  151. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,16 @@
1
1
  from typing import List, Optional
2
2
 
3
- from letta.agent import Agent, AgentState
3
+ from letta.agents.base_agent import BaseAgent
4
4
  from letta.interface import AgentInterface
5
5
  from letta.orm import User
6
+ from letta.schemas.agent import AgentState
6
7
  from letta.schemas.letta_message_content import TextContent
7
8
  from letta.schemas.message import Message, MessageCreate
8
9
  from letta.schemas.openai.chat_completion_response import UsageStatistics
9
10
  from letta.schemas.usage import LettaUsageStatistics
10
11
 
11
12
 
12
- class RoundRobinMultiAgent(Agent):
13
+ class RoundRobinMultiAgent(BaseAgent):
13
14
  def __init__(
14
15
  self,
15
16
  interface: AgentInterface,
@@ -3,10 +3,11 @@ import threading
3
3
  from datetime import datetime, timezone
4
4
  from typing import List, Optional
5
5
 
6
- from letta.agent import Agent, AgentState
6
+ from letta.agents.base_agent import BaseAgent
7
7
  from letta.groups.helpers import stringify_message
8
8
  from letta.interface import AgentInterface
9
9
  from letta.orm import User
10
+ from letta.schemas.agent import AgentState
10
11
  from letta.schemas.enums import JobStatus
11
12
  from letta.schemas.job import JobUpdate
12
13
  from letta.schemas.letta_message_content import TextContent
@@ -19,7 +20,7 @@ from letta.services.job_manager import JobManager
19
20
  from letta.services.message_manager import MessageManager
20
21
 
21
22
 
22
- class SleeptimeMultiAgent(Agent):
23
+ class SleeptimeMultiAgent(BaseAgent):
23
24
  def __init__(
24
25
  self,
25
26
  interface: AgentInterface,
@@ -268,7 +268,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
268
268
  prior_messages = []
269
269
  if self.group.sleeptime_agent_frequency:
270
270
  try:
271
- prior_messages = await self.message_manager.list_messages_for_agent_async(
271
+ prior_messages = await self.message_manager.list_messages(
272
272
  agent_id=foreground_agent_id,
273
273
  actor=self.actor,
274
274
  after=last_processed_message_id,
@@ -7,14 +7,14 @@ from letta.constants import DEFAULT_MAX_STEPS
7
7
  from letta.groups.helpers import stringify_message
8
8
  from letta.otel.tracing import trace_method
9
9
  from letta.schemas.agent import AgentState
10
- from letta.schemas.enums import JobStatus
10
+ from letta.schemas.enums import JobStatus, RunStatus
11
11
  from letta.schemas.group import Group, ManagerType
12
12
  from letta.schemas.job import JobUpdate
13
13
  from letta.schemas.letta_message import MessageType
14
14
  from letta.schemas.letta_message_content import TextContent
15
15
  from letta.schemas.letta_response import LettaResponse
16
16
  from letta.schemas.message import Message, MessageCreate
17
- from letta.schemas.run import Run
17
+ from letta.schemas.run import Run, RunUpdate
18
18
  from letta.schemas.user import User
19
19
  from letta.services.group_manager import GroupManager
20
20
  from letta.utils import safe_create_task
@@ -134,14 +134,14 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
134
134
  use_assistant_message: bool = True,
135
135
  ) -> str:
136
136
  run = Run(
137
- user_id=self.actor.id,
138
- status=JobStatus.created,
137
+ agent_id=sleeptime_agent_id,
138
+ status=RunStatus.created,
139
139
  metadata={
140
- "job_type": "sleeptime_agent_send_message_async", # is this right?
140
+ "run_type": "sleeptime_agent_send_message_async", # is this right?
141
141
  "agent_id": sleeptime_agent_id,
142
142
  },
143
143
  )
144
- run = await self.job_manager.create_job_async(pydantic_job=run, actor=self.actor)
144
+ run = await self.run_manager.create_run(pydantic_run=run, actor=self.actor)
145
145
 
146
146
  safe_create_task(
147
147
  self._participant_agent_step(
@@ -167,15 +167,15 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
167
167
  use_assistant_message: bool = True,
168
168
  ) -> LettaResponse:
169
169
  try:
170
- # Update job status
171
- job_update = JobUpdate(status=JobStatus.running)
172
- await self.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=self.actor)
170
+ # Update run status
171
+ run_update = RunUpdate(status=RunStatus.running)
172
+ await self.run_manager.update_run_by_id_async(run_id=run_id, update=run_update, actor=self.actor)
173
173
 
174
174
  # Create conversation transcript
175
175
  prior_messages = []
176
176
  if self.group.sleeptime_agent_frequency:
177
177
  try:
178
- prior_messages = await self.message_manager.list_messages_for_agent_async(
178
+ prior_messages = await self.message_manager.list_messages(
179
179
  agent_id=foreground_agent_id,
180
180
  actor=self.actor,
181
181
  after=last_processed_message_id,
@@ -212,22 +212,22 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
212
212
  use_assistant_message=use_assistant_message,
213
213
  )
214
214
 
215
- # Update job status
216
- job_update = JobUpdate(
217
- status=JobStatus.completed,
215
+ # Update run status
216
+ run_update = RunUpdate(
217
+ status=RunStatus.completed,
218
218
  completed_at=datetime.now(timezone.utc).replace(tzinfo=None),
219
219
  metadata={
220
220
  "result": result.model_dump(mode="json"),
221
221
  "agent_id": sleeptime_agent_state.id,
222
222
  },
223
223
  )
224
- await self.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=self.actor)
224
+ await self.run_manager.update_run_by_id_async(run_id=run_id, update=run_update, actor=self.actor)
225
225
  return result
226
226
  except Exception as e:
227
- job_update = JobUpdate(
228
- status=JobStatus.failed,
227
+ run_update = RunUpdate(
228
+ status=RunStatus.failed,
229
229
  completed_at=datetime.now(timezone.utc).replace(tzinfo=None),
230
230
  metadata={"error": str(e)},
231
231
  )
232
- await self.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=self.actor)
232
+ await self.run_manager.update_run_by_id_async(run_id=run_id, update=run_update, actor=self.actor)
233
233
  raise
@@ -1,12 +1,13 @@
1
1
  from typing import List, Optional
2
2
 
3
- from letta.agent import Agent, AgentState
3
+ from letta.agents.base_agent import BaseAgent
4
4
  from letta.constants import DEFAULT_MESSAGE_TOOL
5
5
  from letta.functions.function_sets.multi_agent import send_message_to_all_agents_in_group
6
6
  from letta.functions.functions import parse_source_code
7
7
  from letta.functions.schema_generator import generate_schema
8
8
  from letta.interface import AgentInterface
9
9
  from letta.orm import User
10
+ from letta.schemas.agent import AgentState
10
11
  from letta.schemas.enums import ToolType
11
12
  from letta.schemas.letta_message_content import TextContent
12
13
  from letta.schemas.message import MessageCreate
@@ -17,7 +18,7 @@ from letta.services.agent_manager import AgentManager
17
18
  from letta.services.tool_manager import ToolManager
18
19
 
19
20
 
20
- class SupervisorMultiAgent(Agent):
21
+ class SupervisorMultiAgent(BaseAgent):
21
22
  def __init__(
22
23
  self,
23
24
  interface: AgentInterface,
@@ -35,82 +36,85 @@ class SupervisorMultiAgent(Agent):
35
36
  self.agent_manager = AgentManager()
36
37
  self.tool_manager = ToolManager()
37
38
 
38
- def step(
39
- self,
40
- input_messages: List[MessageCreate],
41
- chaining: bool = True,
42
- max_chaining_steps: Optional[int] = None,
43
- put_inner_thoughts_first: bool = True,
44
- assistant_message_tool_name: str = DEFAULT_MESSAGE_TOOL,
45
- **kwargs,
46
- ) -> LettaUsageStatistics:
47
- # Load settings
48
- token_streaming = self.interface.streaming_mode if hasattr(self.interface, "streaming_mode") else False
49
- metadata = self.interface.metadata if hasattr(self.interface, "metadata") else None
50
-
51
- # Prepare supervisor agent
52
- if self.tool_manager.get_tool_by_name(tool_name="send_message_to_all_agents_in_group", actor=self.user) is None:
53
- multi_agent_tool = Tool(
54
- name=send_message_to_all_agents_in_group.__name__,
55
- description="",
56
- source_type="python",
57
- tags=[],
58
- source_code=parse_source_code(send_message_to_all_agents_in_group),
59
- json_schema=generate_schema(send_message_to_all_agents_in_group, None),
60
- )
61
- multi_agent_tool.tool_type = ToolType.LETTA_MULTI_AGENT_CORE
62
- multi_agent_tool = self.tool_manager.create_or_update_tool(
63
- pydantic_tool=multi_agent_tool,
64
- actor=self.user,
65
- )
66
- self.agent_state = self.agent_manager.attach_tool(agent_id=self.agent_state.id, tool_id=multi_agent_tool.id, actor=self.user)
67
-
68
- old_tool_rules = self.agent_state.tool_rules
69
- self.agent_state.tool_rules = [
70
- InitToolRule(
71
- tool_name="send_message_to_all_agents_in_group",
72
- ),
73
- TerminalToolRule(
74
- tool_name=assistant_message_tool_name,
75
- ),
76
- ChildToolRule(
77
- tool_name="send_message_to_all_agents_in_group",
78
- children=[assistant_message_tool_name],
79
- ),
80
- ]
81
-
82
- # Prepare new messages
83
- new_messages = []
84
- for message in input_messages:
85
- if isinstance(message.content, str):
86
- message.content = [TextContent(text=message.content)]
87
- message.group_id = self.group_id
88
- new_messages.append(message)
89
-
90
- try:
91
- # Load supervisor agent
92
- supervisor_agent = Agent(
93
- agent_state=self.agent_state,
94
- interface=self.interface,
95
- user=self.user,
96
- )
97
-
98
- # Perform supervisor step
99
- usage_stats = supervisor_agent.step(
100
- input_messages=new_messages,
101
- chaining=chaining,
102
- max_chaining_steps=max_chaining_steps,
103
- stream=token_streaming,
104
- skip_verify=True,
105
- metadata=metadata,
106
- put_inner_thoughts_first=put_inner_thoughts_first,
107
- )
108
- except Exception as e:
109
- raise e
110
- finally:
111
- self.interface.step_yield()
112
- self.agent_state.tool_rules = old_tool_rules
113
-
114
- self.interface.step_complete()
115
39
 
116
- return usage_stats
40
+ #
41
+ # def step(
42
+ # self,
43
+ # input_messages: List[MessageCreate],
44
+ # chaining: bool = True,
45
+ # max_chaining_steps: Optional[int] = None,
46
+ # put_inner_thoughts_first: bool = True,
47
+ # assistant_message_tool_name: str = DEFAULT_MESSAGE_TOOL,
48
+ # **kwargs,
49
+ # ) -> LettaUsageStatistics:
50
+ # # Load settings
51
+ # token_streaming = self.interface.streaming_mode if hasattr(self.interface, "streaming_mode") else False
52
+ # metadata = self.interface.metadata if hasattr(self.interface, "metadata") else None
53
+ #
54
+ # # Prepare supervisor agent
55
+ # if self.tool_manager.get_tool_by_name(tool_name="send_message_to_all_agents_in_group", actor=self.user) is None:
56
+ # multi_agent_tool = Tool(
57
+ # name=send_message_to_all_agents_in_group.__name__,
58
+ # description="",
59
+ # source_type="python",
60
+ # tags=[],
61
+ # source_code=parse_source_code(send_message_to_all_agents_in_group),
62
+ # json_schema=generate_schema(send_message_to_all_agents_in_group, None),
63
+ # )
64
+ # multi_agent_tool.tool_type = ToolType.LETTA_MULTI_AGENT_CORE
65
+ # multi_agent_tool = self.tool_manager.create_or_update_tool(
66
+ # pydantic_tool=multi_agent_tool,
67
+ # actor=self.user,
68
+ # )
69
+ # self.agent_state = self.agent_manager.attach_tool(agent_id=self.agent_state.id, tool_id=multi_agent_tool.id, actor=self.user)
70
+ #
71
+ # old_tool_rules = self.agent_state.tool_rules
72
+ # self.agent_state.tool_rules = [
73
+ # InitToolRule(
74
+ # tool_name="send_message_to_all_agents_in_group",
75
+ # ),
76
+ # TerminalToolRule(
77
+ # tool_name=assistant_message_tool_name,
78
+ # ),
79
+ # ChildToolRule(
80
+ # tool_name="send_message_to_all_agents_in_group",
81
+ # children=[assistant_message_tool_name],
82
+ # ),
83
+ # ]
84
+ #
85
+ # # Prepare new messages
86
+ # new_messages = []
87
+ # for message in input_messages:
88
+ # if isinstance(message.content, str):
89
+ # message.content = [TextContent(text=message.content)]
90
+ # message.group_id = self.group_id
91
+ # new_messages.append(message)
92
+ #
93
+ # try:
94
+ # # Load supervisor agent
95
+ # supervisor_agent = Agent(
96
+ # agent_state=self.agent_state,
97
+ # interface=self.interface,
98
+ # user=self.user,
99
+ # )
100
+ #
101
+ # # Perform supervisor step
102
+ # usage_stats = supervisor_agent.step(
103
+ # input_messages=new_messages,
104
+ # chaining=chaining,
105
+ # max_chaining_steps=max_chaining_steps,
106
+ # stream=token_streaming,
107
+ # skip_verify=True,
108
+ # metadata=metadata,
109
+ # put_inner_thoughts_first=put_inner_thoughts_first,
110
+ # )
111
+ # except Exception as e:
112
+ # raise e
113
+ # finally:
114
+ # self.interface.step_yield()
115
+ # self.agent_state.tool_rules = old_tool_rules
116
+ #
117
+ # self.interface.step_complete()
118
+ #
119
+ # return usage_stats
120
+ #
@@ -16,6 +16,7 @@ from letta.schemas.letta_message_content import (
16
16
  OmittedReasoningContent,
17
17
  ReasoningContent,
18
18
  RedactedReasoningContent,
19
+ SummarizedReasoningContent,
19
20
  TextContent,
20
21
  ToolCallContent,
21
22
  ToolReturnContent,
@@ -270,6 +271,8 @@ def deserialize_message_content(data: Optional[List[Dict]]) -> List[MessageConte
270
271
  content = RedactedReasoningContent(**item)
271
272
  elif content_type == MessageContentType.omitted_reasoning:
272
273
  content = OmittedReasoningContent(**item)
274
+ elif content_type == MessageContentType.summarized_reasoning:
275
+ content = SummarizedReasoningContent(**item)
273
276
  else:
274
277
  # Skip invalid content
275
278
  continue
@@ -13,6 +13,7 @@ def convert_message_creates_to_messages(
13
13
  message_creates: list[MessageCreate],
14
14
  agent_id: str,
15
15
  timezone: str,
16
+ run_id: str,
16
17
  wrap_user_message: bool = True,
17
18
  wrap_system_message: bool = True,
18
19
  ) -> list[Message]:
@@ -21,6 +22,7 @@ def convert_message_creates_to_messages(
21
22
  message_create=create,
22
23
  agent_id=agent_id,
23
24
  timezone=timezone,
25
+ run_id=run_id,
24
26
  wrap_user_message=wrap_user_message,
25
27
  wrap_system_message=wrap_system_message,
26
28
  )
@@ -32,6 +34,7 @@ def _convert_message_create_to_message(
32
34
  message_create: MessageCreate,
33
35
  agent_id: str,
34
36
  timezone: str,
37
+ run_id: str,
35
38
  wrap_user_message: bool = True,
36
39
  wrap_system_message: bool = True,
37
40
  ) -> Message:
@@ -81,4 +84,5 @@ def _convert_message_create_to_message(
81
84
  sender_id=message_create.sender_id,
82
85
  group_id=message_create.group_id,
83
86
  batch_item_id=message_create.batch_item_id,
87
+ run_id=run_id,
84
88
  )
@@ -50,6 +50,16 @@ class ToolRulesSolver(BaseModel):
50
50
  )
51
51
  tool_call_history: list[str] = Field(default_factory=list, description="History of tool calls, updated with each tool call.")
52
52
 
53
+ # Last-evaluated prefilled args cache (per step)
54
+ last_prefilled_args_by_tool: dict[str, dict] = Field(
55
+ default_factory=dict, description="Cached mapping of tool name to prefilled args from the last allowlist evaluation.", exclude=True
56
+ )
57
+ last_prefilled_args_provenance: dict[str, str] = Field(
58
+ default_factory=dict,
59
+ description="Cached mapping of tool name to a short description of which rule provided the prefilled args.",
60
+ exclude=True,
61
+ )
62
+
53
63
  def __init__(self, tool_rules: list[ToolRule] | None = None, **kwargs):
54
64
  super().__init__(tool_rules=tool_rules, **kwargs)
55
65
 
@@ -88,28 +98,78 @@ class ToolRulesSolver(BaseModel):
88
98
  ) -> list[ToolName]:
89
99
  """Get a list of tool names allowed based on the last tool called.
90
100
 
101
+ Side-effect: also caches any prefilled args provided by active rules into
102
+ `last_prefilled_args_by_tool` and `last_prefilled_args_provenance`.
103
+
91
104
  The logic is as follows:
92
105
  1. if there are no previous tool calls, and we have InitToolRules, those are the only options for the first tool call
93
106
  2. else we take the intersection of the Parent/Child/Conditional/MaxSteps as the options
94
107
  3. Continue/Terminal/RequiredBeforeExit rules are applied in the agent loop flow, not to restrict tools
95
108
  """
96
- # TODO: This piece of code here is quite ugly and deserves a refactor
97
- # TODO: -> Tool rules should probably be refactored to take in a set of tool names?
109
+ # Compute allowed tools first
98
110
  if not self.tool_call_history and self.init_tool_rules:
99
- return [rule.tool_name for rule in self.init_tool_rules]
111
+ allowed = [rule.tool_name for rule in self.init_tool_rules]
100
112
  else:
101
113
  valid_tool_sets = []
102
114
  for rule in self.child_based_tool_rules + self.parent_tool_rules:
103
115
  tools = rule.get_valid_tools(self.tool_call_history, available_tools, last_function_response)
104
116
  valid_tool_sets.append(tools)
105
117
 
106
- # Compute intersection of all valid tool sets
118
+ # Compute intersection of all valid tool sets and restrict to available_tools
107
119
  final_allowed_tools = set.intersection(*valid_tool_sets) if valid_tool_sets else available_tools
120
+ final_allowed_tools = final_allowed_tools & available_tools
108
121
 
109
122
  if error_on_empty and not final_allowed_tools:
110
123
  raise ValueError("No valid tools found based on tool rules.")
111
124
 
112
- return list(final_allowed_tools)
125
+ allowed = list(final_allowed_tools)
126
+
127
+ # Build prefilled args cache for current allowed set
128
+ args_by_tool: dict[str, dict] = {}
129
+ provenance_by_tool: dict[str, str] = {}
130
+
131
+ def _store_args(tool_name: str, args: dict, provenance: str):
132
+ if not isinstance(args, dict) or len(args) == 0:
133
+ return
134
+ if tool_name not in args_by_tool:
135
+ args_by_tool[tool_name] = {}
136
+ args_by_tool[tool_name].update(args) # last-write-wins
137
+ provenance_by_tool[tool_name] = provenance
138
+
139
+ # For caching, restrict to actually available tools
140
+ allowed_set = set(allowed) & available_tools
141
+
142
+ last_tool = self.tool_call_history[-1] if self.tool_call_history else None
143
+
144
+ # Init rule args apply only at the beginning
145
+ if not self.tool_call_history and self.init_tool_rules:
146
+ for rule in self.init_tool_rules:
147
+ if hasattr(rule, "args") and getattr(rule, "args") and rule.tool_name in allowed_set:
148
+ _store_args(rule.tool_name, getattr(rule, "args"), f"InitToolRule({rule.tool_name})")
149
+
150
+ # ChildToolRule per-child args apply only when parent is the last tool
151
+ for rule in self.child_based_tool_rules:
152
+ if isinstance(rule, ChildToolRule) and last_tool == rule.tool_name:
153
+ child_map = rule.get_child_args_map()
154
+ for child_name, child_args in child_map.items():
155
+ if child_name in allowed_set:
156
+ _store_args(child_name, child_args, f"ChildToolRule({rule.tool_name}->{child_name})")
157
+
158
+ # Rule-level args for other rule types (future-proofing)
159
+ for rule in (
160
+ self.parent_tool_rules
161
+ + self.continue_tool_rules
162
+ + self.terminal_tool_rules
163
+ + self.required_before_exit_tool_rules
164
+ + self.requires_approval_tool_rules
165
+ ):
166
+ if hasattr(rule, "args") and getattr(rule, "args") and getattr(rule, "tool_name", None) in allowed_set:
167
+ _store_args(rule.tool_name, getattr(rule, "args"), f"{rule.__class__.__name__}({rule.tool_name})")
168
+
169
+ self.last_prefilled_args_by_tool = args_by_tool
170
+ self.last_prefilled_args_provenance = provenance_by_tool
171
+
172
+ return allowed
113
173
 
114
174
  def is_terminal_tool(self, tool_name: ToolName) -> bool:
115
175
  """Check if the tool is defined as a terminal tool in the terminal tool rules or required-before-exit tool rules."""
@@ -209,3 +269,30 @@ class ToolRulesSolver(BaseModel):
209
269
  violated_rules.append(rendered_prompt)
210
270
 
211
271
  return violated_rules
272
+
273
+ def should_force_tool_call(self) -> bool:
274
+ """
275
+ Determine if a tool call should be forced (using 'required' instead of 'auto') based on active constrained tool rules.
276
+
277
+ Returns:
278
+ bool: True if a constrained tool rule is currently active, False otherwise
279
+ """
280
+ # check if we're at the start with init rules
281
+ if not self.tool_call_history and self.init_tool_rules:
282
+ return True
283
+
284
+ # check if any constrained rule is currently active
285
+ if self.tool_call_history:
286
+ last_tool = self.tool_call_history[-1]
287
+
288
+ # check child-based rules (ChildToolRule, ConditionalToolRule)
289
+ for rule in self.child_based_tool_rules:
290
+ if rule.requires_force_tool_call and rule.tool_name == last_tool:
291
+ return True
292
+
293
+ # check parent rules, `requires_force_tool_call` for safety in case this gets expanded
294
+ for rule in self.parent_tool_rules:
295
+ if rule.requires_force_tool_call and rule.tool_name == last_tool:
296
+ return True
297
+
298
+ return False