letta-nightly 0.11.7.dev20251007104119__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.
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/letta_llm_request_adapter.py +0 -1
- letta/adapters/letta_llm_stream_adapter.py +7 -2
- letta/adapters/simple_llm_request_adapter.py +88 -0
- letta/adapters/simple_llm_stream_adapter.py +192 -0
- letta/agents/agent_loop.py +6 -0
- letta/agents/ephemeral_summary_agent.py +2 -1
- letta/agents/helpers.py +142 -6
- letta/agents/letta_agent.py +13 -33
- letta/agents/letta_agent_batch.py +2 -4
- letta/agents/letta_agent_v2.py +87 -77
- letta/agents/letta_agent_v3.py +899 -0
- letta/agents/voice_agent.py +2 -6
- letta/constants.py +8 -4
- letta/errors.py +40 -0
- letta/functions/function_sets/base.py +84 -4
- letta/functions/function_sets/multi_agent.py +0 -3
- letta/functions/schema_generator.py +113 -71
- letta/groups/dynamic_multi_agent.py +3 -2
- letta/groups/helpers.py +1 -2
- letta/groups/round_robin_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/groups/sleeptime_multi_agent_v3.py +17 -17
- letta/groups/supervisor_multi_agent.py +84 -80
- letta/helpers/converters.py +3 -0
- letta/helpers/message_helper.py +4 -0
- letta/helpers/tool_rule_solver.py +92 -5
- letta/interfaces/anthropic_streaming_interface.py +409 -0
- letta/interfaces/gemini_streaming_interface.py +296 -0
- letta/interfaces/openai_streaming_interface.py +752 -1
- letta/llm_api/anthropic_client.py +126 -16
- letta/llm_api/bedrock_client.py +4 -2
- letta/llm_api/deepseek_client.py +4 -1
- letta/llm_api/google_vertex_client.py +123 -42
- letta/llm_api/groq_client.py +4 -1
- letta/llm_api/llm_api_tools.py +11 -4
- letta/llm_api/llm_client_base.py +6 -2
- letta/llm_api/openai.py +32 -2
- letta/llm_api/openai_client.py +423 -18
- letta/llm_api/xai_client.py +4 -1
- letta/main.py +9 -5
- letta/memory.py +1 -0
- letta/orm/__init__.py +1 -1
- letta/orm/agent.py +10 -0
- letta/orm/block.py +7 -16
- letta/orm/blocks_agents.py +8 -2
- letta/orm/files_agents.py +2 -0
- letta/orm/job.py +7 -5
- letta/orm/mcp_oauth.py +1 -0
- letta/orm/message.py +21 -6
- letta/orm/organization.py +2 -0
- letta/orm/provider.py +6 -2
- letta/orm/run.py +71 -0
- letta/orm/sandbox_config.py +7 -1
- letta/orm/sqlalchemy_base.py +0 -306
- letta/orm/step.py +6 -5
- letta/orm/step_metrics.py +5 -5
- letta/otel/tracing.py +28 -3
- letta/plugins/defaults.py +4 -4
- letta/prompts/system_prompts/__init__.py +2 -0
- letta/prompts/system_prompts/letta_v1.py +25 -0
- letta/schemas/agent.py +3 -2
- letta/schemas/agent_file.py +9 -3
- letta/schemas/block.py +23 -10
- letta/schemas/enums.py +21 -2
- letta/schemas/job.py +17 -4
- letta/schemas/letta_message_content.py +71 -2
- letta/schemas/letta_stop_reason.py +5 -5
- letta/schemas/llm_config.py +53 -3
- letta/schemas/memory.py +1 -1
- letta/schemas/message.py +504 -117
- letta/schemas/openai/responses_request.py +64 -0
- letta/schemas/providers/__init__.py +2 -0
- letta/schemas/providers/anthropic.py +16 -0
- letta/schemas/providers/ollama.py +115 -33
- letta/schemas/providers/openrouter.py +52 -0
- letta/schemas/providers/vllm.py +2 -1
- letta/schemas/run.py +48 -42
- letta/schemas/step.py +2 -2
- letta/schemas/step_metrics.py +1 -1
- letta/schemas/tool.py +15 -107
- letta/schemas/tool_rule.py +88 -5
- letta/serialize_schemas/marshmallow_agent.py +1 -0
- letta/server/db.py +86 -408
- letta/server/rest_api/app.py +61 -10
- letta/server/rest_api/dependencies.py +14 -0
- letta/server/rest_api/redis_stream_manager.py +19 -8
- letta/server/rest_api/routers/v1/agents.py +364 -292
- letta/server/rest_api/routers/v1/blocks.py +14 -20
- letta/server/rest_api/routers/v1/identities.py +45 -110
- letta/server/rest_api/routers/v1/internal_templates.py +21 -0
- letta/server/rest_api/routers/v1/jobs.py +23 -6
- letta/server/rest_api/routers/v1/messages.py +1 -1
- letta/server/rest_api/routers/v1/runs.py +126 -85
- letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
- letta/server/rest_api/routers/v1/tools.py +281 -594
- letta/server/rest_api/routers/v1/voice.py +1 -1
- letta/server/rest_api/streaming_response.py +29 -29
- letta/server/rest_api/utils.py +122 -64
- letta/server/server.py +160 -887
- letta/services/agent_manager.py +236 -919
- letta/services/agent_serialization_manager.py +16 -0
- letta/services/archive_manager.py +0 -100
- letta/services/block_manager.py +211 -168
- letta/services/file_manager.py +1 -1
- letta/services/files_agents_manager.py +24 -33
- letta/services/group_manager.py +0 -142
- letta/services/helpers/agent_manager_helper.py +7 -2
- letta/services/helpers/run_manager_helper.py +85 -0
- letta/services/job_manager.py +96 -411
- letta/services/lettuce/__init__.py +6 -0
- letta/services/lettuce/lettuce_client_base.py +86 -0
- letta/services/mcp_manager.py +38 -6
- letta/services/message_manager.py +165 -362
- letta/services/organization_manager.py +0 -36
- letta/services/passage_manager.py +0 -345
- letta/services/provider_manager.py +0 -80
- letta/services/run_manager.py +301 -0
- letta/services/sandbox_config_manager.py +0 -234
- letta/services/step_manager.py +62 -39
- letta/services/summarizer/summarizer.py +9 -7
- letta/services/telemetry_manager.py +0 -16
- letta/services/tool_executor/builtin_tool_executor.py +35 -0
- letta/services/tool_executor/core_tool_executor.py +397 -2
- letta/services/tool_executor/files_tool_executor.py +3 -3
- letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
- letta/services/tool_executor/tool_execution_manager.py +6 -8
- letta/services/tool_executor/tool_executor_base.py +3 -3
- letta/services/tool_manager.py +85 -339
- letta/services/tool_sandbox/base.py +24 -13
- letta/services/tool_sandbox/e2b_sandbox.py +16 -1
- letta/services/tool_schema_generator.py +123 -0
- letta/services/user_manager.py +0 -99
- letta/settings.py +20 -4
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/METADATA +3 -5
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/RECORD +140 -132
- letta/agents/temporal/activities/__init__.py +0 -4
- letta/agents/temporal/activities/example_activity.py +0 -7
- letta/agents/temporal/activities/prepare_messages.py +0 -10
- letta/agents/temporal/temporal_agent_workflow.py +0 -56
- letta/agents/temporal/types.py +0 -25
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251007104119.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.
|
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(
|
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
|
-
|
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
|
+
#
|
letta/helpers/converters.py
CHANGED
@@ -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
|
letta/helpers/message_helper.py
CHANGED
@@ -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
|
-
#
|
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
|
-
|
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
|
-
|
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
|