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
@@ -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