@@ -268,16 +268,14 @@ def __init__(self, agent: BaseAgent[OutputT]) -> None:
268268 self ._agent_middleware .extend (agent .middleware or [])
269269 self ._agent_middleware .extend (after_user_middlewares )
270270
271- if agent .limits .max_tokens is not None :
272- self ._agent_middleware .append (_TokenLimitMiddleware (agent .limits .max_tokens ))
273271 if agent .limits .max_steps is not None :
274272 self ._agent_middleware .append (_StepLimitMiddleware (agent .limits .max_steps ))
275273 if agent .limits .timeout is not None :
276274 self ._agent_middleware .append (_TimeoutLimitMiddleware (agent .limits .timeout ))
277275
278276 model_impl = _create_langchain_model (agent .model )
279277
280- lc_middleware : list [LC_AgentMiddleware ] = [_Middleware (self ._agent_middleware , model_impl )]
278+ lc_middleware : list [LC_AgentMiddleware ] = [_Middleware (self ._agent_middleware )]
281279
282280 # This middleware is executed just after the tool execution and populates
283281 # the artifact field for failed tool calls, since in such cases we can't
@@ -587,6 +585,41 @@ async def awrap_tool_call(
587585 if _DEBUG :
588586 lc_middleware .append (_DEBUGMiddleware ())
589587
588+ if agent .limits .max_tokens is not None :
589+ # Other limits are implemented using SDK middlewres, but this one
590+ # cannot be easily implemented that way, since count_tokens_approximately needs
591+ # access to list[BaseTool] and the langchain model. We don't expose these
592+ # in our SDK middleware, thus we use the langchain middlewares directly here.
593+ #
594+ # Potentially we could implement count_tokens_approximately puerly using our SDK,
595+ # that would additionally require exposing list[Tool] to AgentState, such that
596+ # middlewares get access to the tools that are passed to LLMs.
597+ #
598+ # This problem should be revisited once we add (potentially) different backends,
599+ # as the middleware-based approach may not generalize well across different backend
600+ # implementations (e.g. other backends could support limit natively, somewhat as
601+ # we do in the public API)
602+
603+ _max_tokens = agent .limits .max_tokens
604+
605+ class _TokenLimitMiddleware (LC_AgentMiddleware ):
606+ @override
607+ async def awrap_model_call (
608+ self ,
609+ request : LC_ModelRequest ,
610+ handler : Callable [[LC_ModelRequest ], Awaitable [LC_ModelCallResult ]],
611+ ) -> LC_ModelCallResult :
612+ token_count = _get_approximate_token_counter (request .model , request .tools )(
613+ request .state ["messages" ]
614+ )
615+
616+ if token_count >= _max_tokens :
617+ raise TokenLimitExceededException (token_limit = _max_tokens )
618+
619+ return await handler (request )
620+
621+ lc_middleware .append (_TokenLimitMiddleware ())
622+
590623 response_format = None
591624 if agent .output_schema is not None :
592625 if _supports_provider_strategy (model_impl ):
@@ -764,11 +797,9 @@ def _prepare_langchain_tools(agent_tools: Sequence[Tool]) -> list[BaseTool]:
764797
765798class _Middleware (LC_AgentMiddleware ):
766799 _middleware : list [AgentMiddleware ]
767- _model : BaseChatModel
768800
769- def __init__ (self , middleware : list [AgentMiddleware ], model : BaseChatModel ) -> None :
801+ def __init__ (self , middleware : list [AgentMiddleware ]) -> None :
770802 self ._middleware = middleware
771- self ._model = model
772803
773804 def _with_model_middleware (
774805 self , model_invoke : ModelMiddlewareHandler
@@ -837,7 +868,7 @@ async def awrap_model_call(
837868 request .state ["messages" ].append (request .runtime .context .retry )
838869 request .runtime .context .retry = False
839870
840- req = _convert_model_request_from_lc (request , self . _model )
871+ req = _convert_model_request_from_lc (request )
841872 final_handler = _convert_model_handler_from_lc (handler , original_request = request )
842873
843874 async def llm_handler (req : ModelRequest ) -> ModelResponse :
@@ -929,7 +960,7 @@ async def awrap_tool_call(
929960 call = _map_tool_call_from_langchain (request .tool_call )
930961
931962 if isinstance (call , ToolCall ):
932- req = _convert_tool_request_from_lc (request , self . _model )
963+ req = _convert_tool_request_from_lc (request )
933964 final_handler = _convert_tool_handler_from_lc (handler , original_request = request )
934965 sdk_response = await self ._with_tool_call_middleware (final_handler )(req )
935966
@@ -955,7 +986,7 @@ async def awrap_tool_call(
955986 artifact = sdk_result ,
956987 )
957988
958- req = _convert_subagent_request_from_lc (request , self . _model )
989+ req = _convert_subagent_request_from_lc (request )
959990 final_handler = _convert_subagent_handler_from_lc (handler , original_request = request )
960991 sdk_response = await self ._with_subagent_call_middleware (final_handler )(req )
961992
@@ -1030,32 +1061,31 @@ async def _sdk_handler(request: ModelRequest) -> ModelResponse:
10301061 return _sdk_handler
10311062
10321063
1033- def _convert_model_request_from_lc (request : LC_ModelRequest , model : BaseChatModel ) -> ModelRequest :
1064+ def _convert_model_request_from_lc (request : LC_ModelRequest ) -> ModelRequest :
10341065 thread_id = request .runtime .context .thread_id
10351066
10361067 system_message = request .system_message .content .__str__ () if request .system_message else ""
10371068
10381069 return ModelRequest (
10391070 system_message = system_message ,
1040- state = _convert_agent_state_from_langchain (request .state , model , thread_id ),
1071+ state = _convert_agent_state_from_langchain (request .state , thread_id ),
10411072 )
10421073
10431074
1044- def _convert_tool_request_from_lc (request : LC_ToolCallRequest , model : BaseChatModel ) -> ToolRequest :
1075+ def _convert_tool_request_from_lc (request : LC_ToolCallRequest ) -> ToolRequest :
10451076 assert isinstance (request .runtime .context , InvokeContext )
10461077 thread_id = request .runtime .context .thread_id
10471078
10481079 tool_call = _map_tool_call_from_langchain (request .tool_call )
10491080 assert isinstance (tool_call , ToolCall ), "Expected tool call"
10501081 return ToolRequest (
10511082 call = tool_call ,
1052- state = _convert_agent_state_from_langchain (request .state , model , thread_id ),
1083+ state = _convert_agent_state_from_langchain (request .state , thread_id ),
10531084 )
10541085
10551086
10561087def _convert_subagent_request_from_lc (
10571088 request : LC_ToolCallRequest ,
1058- model : BaseChatModel ,
10591089) -> SubagentRequest :
10601090 assert isinstance (request .runtime .context , InvokeContext )
10611091 thread_id = request .runtime .context .thread_id
@@ -1064,7 +1094,7 @@ def _convert_subagent_request_from_lc(
10641094 assert isinstance (subagent_call , SubagentCall ), "Expected subagent call"
10651095 return SubagentRequest (
10661096 call = subagent_call ,
1067- state = _convert_agent_state_from_langchain (request .state , model , thread_id ),
1097+ state = _convert_agent_state_from_langchain (request .state , thread_id ),
10681098 )
10691099
10701100
@@ -1732,30 +1762,29 @@ def _map_message_to_langchain(message: BaseMessage) -> LC_AnyMessage:
17321762 raise InvalidMessageTypeError ("Invalid SDK message type" )
17331763
17341764
1735- def _convert_agent_state_from_langchain (
1736- state : LC_AgentState [Any ], model : BaseChatModel , thread_id : str
1737- ) -> AgentState :
1765+ def _convert_agent_state_from_langchain (state : LC_AgentState [Any ], thread_id : str ) -> AgentState :
17381766 messages = state ["messages" ]
1739- total_tokens_counter = _get_approximate_token_counter (model )
1740- total_tokens = total_tokens_counter (messages )
17411767 messages = [_map_message_from_langchain (m ) for m in state ["messages" ]]
17421768 return AgentState (
17431769 messages = messages ,
1744- total_steps = len (messages ),
1745- token_count = total_tokens ,
17461770 thread_id = thread_id ,
17471771 )
17481772
17491773
1750- def _get_approximate_token_counter (model : BaseChatModel ) -> LC_TokenCounter :
1774+ def _get_approximate_token_counter (
1775+ model : BaseChatModel , tools : list [BaseTool | dict [str , Any ]]
1776+ ) -> LC_TokenCounter :
17511777 """Tune parameters of approximate token counter based on model type."""
17521778
1779+ # TODO: consider using use_usage_metadata_scaling option once
1780+ # we expose token usage details from LLMs.
1781+
17531782 # NOTE: This is adapted from the backend provider library
17541783 # 3.3 was estimated in an offline experiment, comparing with Claude's token-counting
17551784 # API: https://platform.claude.com/docs/en/build-with-claude/token-counting
17561785 if model ._llm_type == ANTHROPIC_CHAT_MODEL_TYPE : # pyright: ignore[reportPrivateUsage]
1757- return partial (count_tokens_approximately , chars_per_token = 3.3 )
1758- return count_tokens_approximately
1786+ return partial (count_tokens_approximately , tools = tools , chars_per_token = 3.3 )
1787+ return partial ( count_tokens_approximately , tools = tools )
17591788
17601789
17611790def _create_langchain_model (model : PredefinedModel ) -> BaseChatModel :
@@ -1964,25 +1993,6 @@ def check_tool_name(type: str, name: str) -> None:
19641993 raise _InvalidMessagesException ("last AIMessage has tool calls" )
19651994
19661995
1967- class _TokenLimitMiddleware (AgentMiddleware ):
1968- """Stops agent execution when the token count of messages passed to the model exceeds the given limit."""
1969-
1970- _limit : int
1971-
1972- def __init__ (self , limit : int ) -> None :
1973- self ._limit = limit
1974-
1975- @override
1976- async def model_middleware (
1977- self ,
1978- request : ModelRequest ,
1979- handler : ModelMiddlewareHandler ,
1980- ) -> ModelResponse :
1981- if request .state .token_count >= self ._limit :
1982- raise TokenLimitExceededException (token_limit = self ._limit )
1983- return await handler (request )
1984-
1985-
19861996class _StepLimitMiddleware (AgentMiddleware ):
19871997 """Stops agent execution when the number of steps taken reaches the given limit."""
19881998
@@ -1997,7 +2007,7 @@ async def model_middleware(
19972007 request : ModelRequest ,
19982008 handler : ModelMiddlewareHandler ,
19992009 ) -> ModelResponse :
2000- if request .state .total_steps >= self ._limit :
2010+ if len ( request .state .messages ) >= self ._limit :
20012011 raise StepsLimitExceededException (steps_limit = self ._limit )
20022012 return await handler (request )
20032013
0 commit comments