3232from holmes .core .resource_instruction import ResourceInstructions
3333from holmes .core .runbooks import RunbookManager
3434from holmes .core .safeguards import prevent_overly_repeated_tool_call
35- from holmes .core .tools import StructuredToolResult , ToolResultStatus
35+ from holmes .core .tools import StructuredToolResult , StructuredToolResultStatus
36+ from holmes .core .tools_utils .tool_context_window_limiter import (
37+ prevent_overly_big_tool_response ,
38+ )
3639from holmes .plugins .prompts import load_and_render_prompt
3740from holmes .utils .global_instructions import (
3841 Instructions ,
3942 add_global_instructions_to_user_prompt ,
4043)
4144from holmes .utils .tags import format_tags_in_string , parse_messages_tags
4245from holmes .core .tools_utils .tool_executor import ToolExecutor
46+ from holmes .core .tools_utils .data_types import (
47+ TruncationResult ,
48+ ToolCallResult ,
49+ TruncationMetadata ,
50+ )
4351from holmes .core .tracing import DummySpan
4452from holmes .utils .colors import AI_COLOR
4553from holmes .utils .stream import StreamEvents , StreamMessage
@@ -119,34 +127,6 @@ def _process_cost_info(
119127 logging .debug (f"Could not extract cost information: { e } " )
120128
121129
122- class TruncationMetadata (BaseModel ):
123- tool_call_id : str
124- start_index : int
125- end_index : int
126-
127-
128- class TruncationResult (BaseModel ):
129- truncated_messages : List [dict ]
130- truncations : List [TruncationMetadata ]
131-
132-
133- def format_tool_result_data (tool_result : StructuredToolResult ) -> str :
134- tool_response = tool_result .data
135- if isinstance (tool_result .data , str ):
136- tool_response = tool_result .data
137- else :
138- try :
139- if isinstance (tool_result .data , BaseModel ):
140- tool_response = tool_result .data .model_dump_json (indent = 2 )
141- else :
142- tool_response = json .dumps (tool_result .data , indent = 2 )
143- except Exception :
144- tool_response = str (tool_result .data )
145- if tool_result .status == ToolResultStatus .ERROR :
146- tool_response = f"{ tool_result .error or 'Tool execution failed' } :\n \n { tool_result .data or '' } " .strip ()
147- return tool_response
148-
149-
150130# TODO: I think there's a bug here because we don't account for the 'role' or json structure like '{...}' when counting tokens
151131# However, in practice it works because we reserve enough space for the output tokens that the minor inconsistency does not matter
152132# We should fix this in the future
@@ -249,52 +229,6 @@ def truncate_messages_to_fit_context(
249229 return TruncationResult (truncated_messages = messages , truncations = truncations )
250230
251231
252- class ToolCallResult (BaseModel ):
253- tool_call_id : str
254- tool_name : str
255- description : str
256- result : StructuredToolResult
257- size : Optional [int ] = None
258-
259- def as_tool_call_message (self ):
260- content = format_tool_result_data (self .result )
261- if self .result .params :
262- content = (
263- f"Params used for the tool call: { json .dumps (self .result .params )} . The tool call output follows on the next line.\n "
264- + content
265- )
266- return {
267- "tool_call_id" : self .tool_call_id ,
268- "role" : "tool" ,
269- "name" : self .tool_name ,
270- "content" : content ,
271- }
272-
273- def as_tool_result_response (self ):
274- result_dump = self .result .model_dump ()
275- result_dump ["data" ] = self .result .get_stringified_data ()
276-
277- return {
278- "tool_call_id" : self .tool_call_id ,
279- "tool_name" : self .tool_name ,
280- "description" : self .description ,
281- "role" : "tool" ,
282- "result" : result_dump ,
283- }
284-
285- def as_streaming_tool_result_response (self ):
286- result_dump = self .result .model_dump ()
287- result_dump ["data" ] = self .result .get_stringified_data ()
288-
289- return {
290- "tool_call_id" : self .tool_call_id ,
291- "role" : "tool" ,
292- "description" : self .description ,
293- "name" : self .tool_name ,
294- "result" : result_dump ,
295- }
296-
297-
298232class LLMResult (LLMCosts ):
299233 tool_calls : Optional [List [ToolCallResult ]] = None
300234 result : Optional [str ] = None
@@ -539,7 +473,7 @@ def call( # type: ignore
539473
540474 if (
541475 tool_call_result .result .status
542- == ToolResultStatus .APPROVAL_REQUIRED
476+ == StructuredToolResultStatus .APPROVAL_REQUIRED
543477 ):
544478 with trace_span .start_span (type = "tool" ) as tool_span :
545479 tool_call_result = self ._handle_tool_call_approval (
@@ -577,7 +511,7 @@ def _directly_invoke_tool_call(
577511 f"Skipping tool execution for { tool_name } : args: { tool_params } "
578512 )
579513 return StructuredToolResult (
580- status = ToolResultStatus .ERROR ,
514+ status = StructuredToolResultStatus .ERROR ,
581515 error = f"Failed to find tool { tool_name } " ,
582516 params = tool_params ,
583517 )
@@ -591,7 +525,7 @@ def _directly_invoke_tool_call(
591525 f"Tool call to { tool_name } failed with an Exception" , exc_info = True
592526 )
593527 tool_response = StructuredToolResult (
594- status = ToolResultStatus .ERROR ,
528+ status = StructuredToolResultStatus .ERROR ,
595529 error = f"Tool call failed: { e } " ,
596530 params = tool_params ,
597531 )
@@ -633,7 +567,7 @@ def _get_tool_call_result(
633567 f"Tool { tool_name } return type is not StructuredToolResult. Nesting the tool result into StructuredToolResult..."
634568 )
635569 tool_response = StructuredToolResult (
636- status = ToolResultStatus .SUCCESS ,
570+ status = StructuredToolResultStatus .SUCCESS ,
637571 data = tool_response ,
638572 params = tool_params ,
639573 )
@@ -683,7 +617,7 @@ def _invoke_llm_tool_call(
683617 tool_name = tool_name ,
684618 description = "NA" ,
685619 result = StructuredToolResult (
686- status = ToolResultStatus .ERROR ,
620+ status = StructuredToolResultStatus .ERROR ,
687621 error = "Custom tool calls are not supported" ,
688622 params = None ,
689623 ),
@@ -699,6 +633,11 @@ def _invoke_llm_tool_call(
699633 previous_tool_calls = previous_tool_calls ,
700634 tool_number = tool_number ,
701635 )
636+
637+ prevent_overly_big_tool_response (
638+ tool_call_result = tool_call_result , llm = self .llm
639+ )
640+
702641 ToolCallingLLM ._log_tool_call_result (tool_span , tool_call_result )
703642 return tool_call_result
704643
@@ -720,7 +659,7 @@ def _handle_tool_call_approval(
720659
721660 # If no approval callback, convert to ERROR because it is assumed the client may not be able to handle approvals
722661 if not self .approval_callback :
723- tool_call_result .result .status = ToolResultStatus .ERROR
662+ tool_call_result .result .status = StructuredToolResultStatus .ERROR
724663 return tool_call_result
725664
726665 # Get approval from user
@@ -740,7 +679,7 @@ def _handle_tool_call_approval(
740679 else :
741680 # User denied - update to error
742681 feedback_text = f" User feedback: { feedback } " if feedback else ""
743- tool_call_result .result .status = ToolResultStatus .ERROR
682+ tool_call_result .result .status = StructuredToolResultStatus .ERROR
744683 tool_call_result .result .error = (
745684 f"User denied command execution.{ feedback_text } "
746685 )
@@ -952,7 +891,6 @@ def call_stream(
952891
953892 for future in concurrent .futures .as_completed (futures ):
954893 tool_call_result : ToolCallResult = future .result ()
955-
956894 tool_calls .append (tool_call_result .as_tool_result_response ())
957895 messages .append (tool_call_result .as_tool_call_message ())
958896
0 commit comments