fix: validate parallel tool calls with tool rules at create/update time (#8060)

* fix: validate parallel tool calls with tool rules at create/update time

Move validation from runtime to agent create/update time for better UX.
Add client-side enforcement to truncate parallel tool calls when disabled
(handles providers like Gemini that ignore the setting).

🤖 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

* update apis

* undo

---------

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Sarah Wooders
2025-12-24 17:04:31 -07:00
committed by Caren Thomas
parent 28b2f8b03f
commit 0722877423
2 changed files with 33 additions and 9 deletions

View File

@@ -726,6 +726,15 @@ class LettaAgentV3(LettaAgentV2):
else:
tool_calls = []
# Enforce parallel_tool_calls=false by truncating to first tool call
# Some providers (e.g. Gemini) don't respect this setting via API, so we enforce it client-side
if len(tool_calls) > 1 and not self.agent_state.llm_config.parallel_tool_calls:
self.logger.warning(
f"LLM returned {len(tool_calls)} tool calls but parallel_tool_calls=false. "
f"Truncating to first tool call: {tool_calls[0].function.name}"
)
tool_calls = [tool_calls[0]]
# get the new generated `Message` objects from handling the LLM response
new_messages, self.should_continue, self.stop_reason = await self._handle_ai_response(
tool_calls=tool_calls,
@@ -1037,15 +1046,11 @@ class LettaAgentV3(LettaAgentV2):
# 5. Unified tool execution path (works for both single and multiple tools)
# 5a. Validate parallel tool calling constraints
if len(tool_calls) > 1:
# No parallel tool calls with tool rules
if self.agent_state.tool_rules and len([r for r in self.agent_state.tool_rules if r.type != "requires_approval"]) > 0:
raise ValueError(
"Parallel tool calling is not allowed when tool rules are present. Disable tool rules to use parallel tool calls."
)
# 5. Unified tool execution path (works for both single and multiple tools)
# Note: Parallel tool calling with tool rules is validated at agent create/update time.
# At runtime, we trust that if tool_rules exist, parallel_tool_calls=false is enforced earlier.
# 5b. Prepare execution specs for all tools
# 5a. Prepare execution specs for all tools
exec_specs = []
for tc in tool_calls:
call_id = tc.id or f"call_{uuid.uuid4().hex[:8]}"

View File

@@ -25,7 +25,7 @@ from letta.constants import (
INCLUDE_MODEL_KEYWORDS_BASE_TOOL_RULES,
RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE,
)
from letta.errors import LettaAgentNotFoundError
from letta.errors import LettaAgentNotFoundError, LettaInvalidArgumentError
from letta.helpers import ToolRulesSolver
from letta.helpers.datetime_helpers import get_utc_time
from letta.llm_api.llm_client import LLMClient
@@ -481,6 +481,15 @@ class AgentManager:
if tool_rules:
check_supports_structured_output(model=agent_create.llm_config.model, tool_rules=tool_rules)
# Validate parallel tool calling with tool rules
# Tool rules (excluding requires_approval) require sequential execution
non_approval_rules = [r for r in tool_rules if r.type != "requires_approval"]
if non_approval_rules and agent_create.llm_config.parallel_tool_calls:
raise LettaInvalidArgumentError(
"Parallel tool calling is not allowed when tool rules are present. "
"Set `parallel_tool_calls=False` in llm_config or remove tool rules."
)
new_agent = AgentModel(
name=agent_create.name,
system=derive_system_message(
@@ -772,6 +781,16 @@ class AgentManager:
if val is not None:
setattr(agent, col, val)
# Validate parallel tool calling with tool rules after updates
# Tool rules (excluding requires_approval) require sequential execution
if agent.tool_rules and agent.llm_config.parallel_tool_calls:
non_approval_rules = [r for r in agent.tool_rules if r.type != "requires_approval"]
if non_approval_rules:
raise LettaInvalidArgumentError(
"Parallel tool calling is not allowed when tool rules are present. "
"Set `parallel_tool_calls=False` in llm_config or remove tool rules."
)
if agent_update.metadata is not None:
agent.metadata_ = agent_update.metadata