@@ -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,40 @@ 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) as the middleware-based approach
599+ # may not generalize well across different backend implementations (e.g. other backends
600+ # could support limit natively, somewhat as we do in the public API)
601+
602+ _max_tokens = agent .limits .max_tokens
603+
604+ class _TokenLimitMiddleware (LC_AgentMiddleware ):
605+ @override
606+ async def awrap_model_call (
607+ self ,
608+ request : LC_ModelRequest ,
609+ handler : Callable [[LC_ModelRequest ], Awaitable [LC_ModelCallResult ]],
610+ ) -> LC_ModelCallResult :
611+ token_count = _get_approximate_token_counter (request .model , request .tools )(
612+ request .state ["messages" ]
613+ )
614+
615+ if token_count >= _max_tokens :
616+ raise TokenLimitExceededException (token_limit = _max_tokens )
617+
618+ return await handler (request )
619+
620+ lc_middleware .append (_TokenLimitMiddleware ())
621+
590622 response_format = None
591623 if agent .output_schema is not None :
592624 if _supports_provider_strategy (model_impl ):
@@ -764,11 +796,9 @@ def _prepare_langchain_tools(agent_tools: Sequence[Tool]) -> list[BaseTool]:
764796
765797class _Middleware (LC_AgentMiddleware ):
766798 _middleware : list [AgentMiddleware ]
767- _model : BaseChatModel
768799
769- def __init__ (self , middleware : list [AgentMiddleware ], model : BaseChatModel ) -> None :
800+ def __init__ (self , middleware : list [AgentMiddleware ]) -> None :
770801 self ._middleware = middleware
771- self ._model = model
772802
773803 def _with_model_middleware (
774804 self , model_invoke : ModelMiddlewareHandler
@@ -837,7 +867,7 @@ async def awrap_model_call(
837867 request .state ["messages" ].append (request .runtime .context .retry )
838868 request .runtime .context .retry = False
839869
840- req = _convert_model_request_from_lc (request , self . _model )
870+ req = _convert_model_request_from_lc (request )
841871 final_handler = _convert_model_handler_from_lc (handler , original_request = request )
842872
843873 async def llm_handler (req : ModelRequest ) -> ModelResponse :
@@ -929,7 +959,7 @@ async def awrap_tool_call(
929959 call = _map_tool_call_from_langchain (request .tool_call )
930960
931961 if isinstance (call , ToolCall ):
932- req = _convert_tool_request_from_lc (request , self . _model )
962+ req = _convert_tool_request_from_lc (request )
933963 final_handler = _convert_tool_handler_from_lc (handler , original_request = request )
934964 sdk_response = await self ._with_tool_call_middleware (final_handler )(req )
935965
@@ -955,7 +985,7 @@ async def awrap_tool_call(
955985 artifact = sdk_result ,
956986 )
957987
958- req = _convert_subagent_request_from_lc (request , self . _model )
988+ req = _convert_subagent_request_from_lc (request )
959989 final_handler = _convert_subagent_handler_from_lc (handler , original_request = request )
960990 sdk_response = await self ._with_subagent_call_middleware (final_handler )(req )
961991
@@ -1030,32 +1060,31 @@ async def _sdk_handler(request: ModelRequest) -> ModelResponse:
10301060 return _sdk_handler
10311061
10321062
1033- def _convert_model_request_from_lc (request : LC_ModelRequest , model : BaseChatModel ) -> ModelRequest :
1063+ def _convert_model_request_from_lc (request : LC_ModelRequest ) -> ModelRequest :
10341064 thread_id = request .runtime .context .thread_id
10351065
10361066 system_message = request .system_message .content .__str__ () if request .system_message else ""
10371067
10381068 return ModelRequest (
10391069 system_message = system_message ,
1040- state = _convert_agent_state_from_langchain (request .state , model , thread_id ),
1070+ state = _convert_agent_state_from_langchain (request .state , thread_id ),
10411071 )
10421072
10431073
1044- def _convert_tool_request_from_lc (request : LC_ToolCallRequest , model : BaseChatModel ) -> ToolRequest :
1074+ def _convert_tool_request_from_lc (request : LC_ToolCallRequest ) -> ToolRequest :
10451075 assert isinstance (request .runtime .context , InvokeContext )
10461076 thread_id = request .runtime .context .thread_id
10471077
10481078 tool_call = _map_tool_call_from_langchain (request .tool_call )
10491079 assert isinstance (tool_call , ToolCall ), "Expected tool call"
10501080 return ToolRequest (
10511081 call = tool_call ,
1052- state = _convert_agent_state_from_langchain (request .state , model , thread_id ),
1082+ state = _convert_agent_state_from_langchain (request .state , thread_id ),
10531083 )
10541084
10551085
10561086def _convert_subagent_request_from_lc (
10571087 request : LC_ToolCallRequest ,
1058- model : BaseChatModel ,
10591088) -> SubagentRequest :
10601089 assert isinstance (request .runtime .context , InvokeContext )
10611090 thread_id = request .runtime .context .thread_id
@@ -1064,7 +1093,7 @@ def _convert_subagent_request_from_lc(
10641093 assert isinstance (subagent_call , SubagentCall ), "Expected subagent call"
10651094 return SubagentRequest (
10661095 call = subagent_call ,
1067- state = _convert_agent_state_from_langchain (request .state , model , thread_id ),
1096+ state = _convert_agent_state_from_langchain (request .state , thread_id ),
10681097 )
10691098
10701099
@@ -1732,30 +1761,29 @@ def _map_message_to_langchain(message: BaseMessage) -> LC_AnyMessage:
17321761 raise InvalidMessageTypeError ("Invalid SDK message type" )
17331762
17341763
1735- def _convert_agent_state_from_langchain (
1736- state : LC_AgentState [Any ], model : BaseChatModel , thread_id : str
1737- ) -> AgentState :
1764+ def _convert_agent_state_from_langchain (state : LC_AgentState [Any ], thread_id : str ) -> AgentState :
17381765 messages = state ["messages" ]
1739- total_tokens_counter = _get_approximate_token_counter (model )
1740- total_tokens = total_tokens_counter (messages )
17411766 messages = [_map_message_from_langchain (m ) for m in state ["messages" ]]
17421767 return AgentState (
17431768 messages = messages ,
1744- total_steps = len (messages ),
1745- token_count = total_tokens ,
17461769 thread_id = thread_id ,
17471770 )
17481771
17491772
1750- def _get_approximate_token_counter (model : BaseChatModel ) -> LC_TokenCounter :
1773+ def _get_approximate_token_counter (
1774+ model : BaseChatModel , tools : list [BaseTool | dict [str , Any ]]
1775+ ) -> LC_TokenCounter :
17511776 """Tune parameters of approximate token counter based on model type."""
17521777
1778+ # TODO: consider using use_usage_metadata_scaling option once
1779+ # we expose token usage details from LLMs.
1780+
17531781 # NOTE: This is adapted from the backend provider library
17541782 # 3.3 was estimated in an offline experiment, comparing with Claude's token-counting
17551783 # API: https://platform.claude.com/docs/en/build-with-claude/token-counting
17561784 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
1785+ return partial (count_tokens_approximately , tools = tools , chars_per_token = 3.3 )
1786+ return partial ( count_tokens_approximately , tools = tools )
17591787
17601788
17611789def _create_langchain_model (model : PredefinedModel ) -> BaseChatModel :
@@ -1964,25 +1992,6 @@ def check_tool_name(type: str, name: str) -> None:
19641992 raise _InvalidMessagesException ("last AIMessage has tool calls" )
19651993
19661994
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-
19861995class _StepLimitMiddleware (AgentMiddleware ):
19871996 """Stops agent execution when the number of steps taken reaches the given limit."""
19881997
@@ -1997,7 +2006,7 @@ async def model_middleware(
19972006 request : ModelRequest ,
19982007 handler : ModelMiddlewareHandler ,
19992008 ) -> ModelResponse :
2000- if request .state .total_steps >= self ._limit :
2009+ if len ( request .state .messages ) >= self ._limit :
20012010 raise StepsLimitExceededException (steps_limit = self ._limit )
20022011 return await handler (request )
20032012
0 commit comments