From 396959da2f7c976a245cfe5589af56121adea4c2 Mon Sep 17 00:00:00 2001 From: Matthew Zhou Date: Fri, 17 Oct 2025 15:35:48 -0700 Subject: [PATCH] feat: Add toggle on llm config for parallel tool calling [LET-5610] (#5542) * Add parallel tool calling field * Thread through parallel tool use * Fern autogen * Fix send message v2 --- fern/openapi.json | 67 +++++++++++++++++++++++ letta/agents/letta_agent_v3.py | 6 +- letta/schemas/agent.py | 2 + letta/schemas/llm_config.py | 1 + tests/integration_test_send_message_v2.py | 2 + 5 files changed, 75 insertions(+), 3 deletions(-) diff --git a/fern/openapi.json b/fern/openapi.json index ef1ca950..62c4f0e0 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -14935,6 +14935,36 @@ {} ], "nullable": true + }, + "parallel_tool_calls": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "format": "null", + "nullable": true + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "format": "null", + "nullable": true + } + ], + "nullable": true + } + }, + {} + ], + "nullable": true } }, "required": [ @@ -26223,6 +26253,12 @@ ], "title": "Hidden", "description": "If set to True, the agent will be hidden." + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "If set to True, enables parallel tool calling. Defaults to False.", + "default": false } }, "type": "object", @@ -29530,6 +29566,12 @@ "title": "Hidden", "description": "If set to True, the agent will be hidden." }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "If set to True, enables parallel tool calling. Defaults to False.", + "default": false + }, "deployment_id": { "type": "string", "title": "Deployment Id", @@ -30269,6 +30311,19 @@ ], "title": "Tier", "description": "The cost tier for the model (cloud only)." + }, + "parallel_tool_calls": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Parallel Tool Calls", + "description": "If set to True, enables parallel tool calling. Defaults to False.", + "default": false } }, "type": "object", @@ -36764,6 +36819,12 @@ ], "title": "Hidden", "description": "If set to True, the agent will be hidden." + }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "If set to True, enables parallel tool calling. Defaults to False.", + "default": false } }, "type": "object", @@ -37984,6 +38045,12 @@ "title": "Hidden", "description": "If set to True, the agent will be hidden." }, + "parallel_tool_calls": { + "type": "boolean", + "title": "Parallel Tool Calls", + "description": "If set to True, enables parallel tool calling. Defaults to False.", + "default": false + }, "id": { "type": "string", "title": "Id", diff --git a/letta/agents/letta_agent_v3.py b/letta/agents/letta_agent_v3.py index d73f26f4..d159a193 100644 --- a/letta/agents/letta_agent_v3.py +++ b/letta/agents/letta_agent_v3.py @@ -372,11 +372,11 @@ class LettaAgentV3(LettaAgentV2): isinstance(request_data.get("tool_choice"), dict) and "disable_parallel_tool_use" in request_data["tool_choice"] ): - # Gate parallel tool use on both: no tool rules and no approval-required tools - if no_tool_rules and not has_approval_tools: + # Gate parallel tool use on both: no tool rules and no approval-required tools and toggled on + if no_tool_rules and not has_approval_tools and self.agent_state.llm_config.parallel_tool_calls: request_data["tool_choice"]["disable_parallel_tool_use"] = False else: - # Explicitly disable when approvals exist (TODO support later) or tool rules present + # Explicitly disable when approvals exist (TODO support later) or tool rules present or llm_config toggled off request_data["tool_choice"]["disable_parallel_tool_use"] = True except Exception: # if this fails, we simply don't enable parallel tool use diff --git a/letta/schemas/agent.py b/letta/schemas/agent.py index 657916fe..8b11afcf 100644 --- a/letta/schemas/agent.py +++ b/letta/schemas/agent.py @@ -256,6 +256,7 @@ class CreateAgent(BaseModel, validate_assignment=True): # None, description="If set to True, the agent will be hidden.", ) + parallel_tool_calls: bool = Field(False, description="If set to True, enables parallel tool calling. Defaults to False.") @field_validator("name") @classmethod @@ -371,6 +372,7 @@ class UpdateAgent(BaseModel): None, description="If set to True, the agent will be hidden.", ) + parallel_tool_calls: bool = Field(False, description="If set to True, enables parallel tool calling. Defaults to False.") model_config = ConfigDict(extra="ignore") # Ignores extra fields diff --git a/letta/schemas/llm_config.py b/letta/schemas/llm_config.py index e46362d5..bebfb950 100644 --- a/letta/schemas/llm_config.py +++ b/letta/schemas/llm_config.py @@ -80,6 +80,7 @@ class LLMConfig(BaseModel): # FIXME hack to silence pydantic protected namespace warning model_config = ConfigDict(protected_namespaces=()) + parallel_tool_calls: Optional[bool] = Field(False, description="If set to True, enables parallel tool calling. Defaults to False.") @model_validator(mode="before") @classmethod diff --git a/tests/integration_test_send_message_v2.py b/tests/integration_test_send_message_v2.py index 196338cf..5e98d27a 100644 --- a/tests/integration_test_send_message_v2.py +++ b/tests/integration_test_send_message_v2.py @@ -543,6 +543,8 @@ async def test_parallel_tool_call_anthropic( if llm_config.model_endpoint_type != "anthropic": pytest.skip("Parallel tool calling test only applies to Anthropic models.") + # change llm_config to support parallel tool calling + llm_config.parallel_tool_calls = True agent_state = await client.agents.modify(agent_id=agent_state.id, llm_config=llm_config) if send_type == "step":