From c3152e10c84d7a612354de06735073644bfce73e Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Thu, 12 Mar 2026 03:14:36 -0400 Subject: [PATCH] feat: add agent loop protection - max iterations and tool result truncation - Add max-tool-iterations config (default 16) to prevent runaway agent loops - Add max-tool-result-chars config (default 8000) to truncate oversized tool results - Both settings are configurable in pipeline UI under Local Agent settings - Logs warnings when limits are hit for debugging Closes #2051 --- .../pkg/provider/runners/localagent.py | 22 +++++++++++++++++++ .../templates/metadata/pipeline/ai.yaml | 20 +++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/langbot/pkg/provider/runners/localagent.py b/src/langbot/pkg/provider/runners/localagent.py index 7d45a1f2..c18300e5 100644 --- a/src/langbot/pkg/provider/runners/localagent.py +++ b/src/langbot/pkg/provider/runners/localagent.py @@ -132,6 +132,12 @@ class LocalAgentRunner(runner.RequestRunner): """Run request""" pending_tool_calls = [] + # Agent loop protection config + agent_config = query.pipeline_config['ai']['local-agent'] + max_tool_iterations = agent_config.get('max-tool-iterations', 16) + max_tool_result_chars = agent_config.get('max-tool-result-chars', 8000) + iteration_count = 0 + # Get knowledge bases list (new field) kb_uuids = query.pipeline_config['ai']['local-agent'].get('knowledge-bases', []) @@ -295,6 +301,14 @@ class LocalAgentRunner(runner.RequestRunner): # Once a model succeeds, commit to it for the tool call loop # (no fallback mid-conversation — different models may interpret tool results differently) while pending_tool_calls: + iteration_count += 1 + if iteration_count > max_tool_iterations: + self.ap.logger.warning( + f'localagent: query={query.query_id} agent loop exceeded max iterations ({max_tool_iterations}), ' + f'forcing termination' + ) + break + for tool_call in pending_tool_calls: try: func = tool_call.function @@ -317,6 +331,14 @@ class LocalAgentRunner(runner.RequestRunner): else: tool_content = json.dumps(func_ret, ensure_ascii=False) + # Truncate oversized tool results to prevent context overflow + if isinstance(tool_content, str) and len(tool_content) > max_tool_result_chars: + self.ap.logger.warning( + f'localagent: tool {func.name} returned {len(tool_content)} chars, ' + f'truncating to {max_tool_result_chars}' + ) + tool_content = tool_content[:max_tool_result_chars] + '\n...[result truncated]' + if is_stream: msg = provider_message.MessageChunk( role='tool', diff --git a/src/langbot/templates/metadata/pipeline/ai.yaml b/src/langbot/templates/metadata/pipeline/ai.yaml index 46f5d463..e290ed6d 100644 --- a/src/langbot/templates/metadata/pipeline/ai.yaml +++ b/src/langbot/templates/metadata/pipeline/ai.yaml @@ -93,6 +93,26 @@ stages: type: knowledge-base-multi-selector required: false default: [] + - name: max-tool-iterations + label: + en_US: Max Tool Iterations + zh_Hans: 最大工具调用轮次 + description: + en_US: Maximum number of tool call iterations in a single agent loop to prevent runaway loops + zh_Hans: 单次 Agent 循环中工具调用的最大轮次,防止无限循环 + type: integer + required: false + default: 16 + - name: max-tool-result-chars + label: + en_US: Max Tool Result Length + zh_Hans: 工具返回最大字符数 + description: + en_US: Maximum character length of a single tool call result, longer results will be truncated + zh_Hans: 单次工具调用返回结果的最大字符数,超出部分将被截断 + type: integer + required: false + default: 8000 - name: tbox-app-api label: en_US: Tbox App API