This commit is contained in:
2026-02-21 22:40:50 -05:00
parent 478dbb9b86
commit 00d14131a9

View File

@@ -27,7 +27,8 @@ comms_log_callback = None
# Signature: (script: str, result: str) -> None # Signature: (script: str, result: str) -> None
tool_log_callback = None tool_log_callback = None
MAX_TOOL_ROUNDS = 5 # Increased to allow thorough code exploration before forcing a summary
MAX_TOOL_ROUNDS = 10
# Maximum characters per text chunk sent to Anthropic. # Maximum characters per text chunk sent to Anthropic.
# Kept well under the ~200k token API limit. # Kept well under the ~200k token API limit.
@@ -438,7 +439,8 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str, file_items:
all_text_parts = [] all_text_parts = []
for round_idx in range(MAX_TOOL_ROUNDS + 1): # We allow MAX_TOOL_ROUNDS, plus 1 final loop to get the text synthesis
for round_idx in range(MAX_TOOL_ROUNDS + 2):
response = _gemini_chat.send_message(payload_to_send) response = _gemini_chat.send_message(payload_to_send)
text_parts_raw = [ text_parts_raw = [
@@ -483,53 +485,48 @@ def _send_gemini(md_content: str, user_message: str, base_dir: str, file_items:
if not tool_calls: if not tool_calls:
break break
if round_idx >= MAX_TOOL_ROUNDS: if round_idx > MAX_TOOL_ROUNDS:
# The model ignored the MAX ROUNDS warning and kept calling tools.
# Force abort to prevent infinite loop.
break break
function_responses = [] function_responses = []
sent_results_log = [] sent_results_log = []
for fc in tool_calls:
for i, fc in enumerate(tool_calls):
fc_name = fc.name fc_name = fc.name
fc_args = dict(fc.args) fc_args = dict(fc.args)
if fc_name in mcp_client.TOOL_NAMES: if fc_name in mcp_client.TOOL_NAMES:
_append_comms("OUT", "tool_call", {"name": fc_name, "args": fc_args}) _append_comms("OUT", "tool_call", {"name": fc_name, "args": fc_args})
output = mcp_client.dispatch(fc_name, fc_args) output = mcp_client.dispatch(fc_name, fc_args)
_append_comms("IN", "tool_result", {"name": fc_name, "output": output}) _append_comms("IN", "tool_result", {"name": fc_name, "output": output})
function_responses.append(
types.Part.from_function_response(
name=fc_name,
response={"output": output}
)
)
sent_results_log.append({"tool_use_id": fc_name, "content": output})
elif fc_name == TOOL_NAME: elif fc_name == TOOL_NAME:
script = fc_args.get("script", "") script = fc_args.get("script", "")
_append_comms("OUT", "tool_call", { _append_comms("OUT", "tool_call", {"name": TOOL_NAME, "script": script})
"name": TOOL_NAME,
"script": script,
})
output = _run_script(script, base_dir) output = _run_script(script, base_dir)
_append_comms("IN", "tool_result", { _append_comms("IN", "tool_result", {"name": TOOL_NAME, "output": output})
"name": TOOL_NAME, else:
"output": output, output = f"ERROR: unknown tool '{fc_name}'"
})
function_responses.append(
types.Part.from_function_response(
name=TOOL_NAME,
response={"output": output}
)
)
sent_results_log.append({"tool_use_id": TOOL_NAME, "content": output})
# Refresh file context after tool calls locally, but DO NOT inject as text part into Gemini. # Inject dynamic updates directly into the LAST tool's output string.
# Gemini strictly expects only function_responses in this array. # Gemini strictly expects function_responses only, so we piggyback on the string.
if file_items: if i == len(tool_calls) - 1:
file_items = _reread_file_items(file_items) if file_items:
file_items = _reread_file_items(file_items)
refreshed_ctx = _build_file_context_text(file_items)
if refreshed_ctx:
output += f"\n\n[SYSTEM: FILES UPDATED — current contents below. Do NOT re-read these files.]\n\n{refreshed_ctx}"
if round_idx == MAX_TOOL_ROUNDS:
output += "\n\n[SYSTEM WARNING: MAX TOOL ROUNDS REACHED. YOU MUST PROVIDE YOUR FINAL ANSWER NOW WITHOUT CALLING ANY MORE TOOLS.]"
_append_comms("OUT", "tool_result_send", { function_responses.append(
"results": sent_results_log types.Part.from_function_response(name=fc_name, response={"output": output})
}) )
sent_results_log.append({"tool_use_id": fc_name, "content": output})
_append_comms("OUT", "tool_result_send", {"results": sent_results_log})
payload_to_send = function_responses payload_to_send = function_responses
final_text = "\n\n".join(all_text_parts) final_text = "\n\n".join(all_text_parts)
@@ -641,7 +638,8 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
all_text_parts = [] all_text_parts = []
for round_idx in range(MAX_TOOL_ROUNDS + 1): # We allow MAX_TOOL_ROUNDS, plus 1 final loop to get the text synthesis
for round_idx in range(MAX_TOOL_ROUNDS + 2):
response = _anthropic_client.messages.create( response = _anthropic_client.messages.create(
model=_model, model=_model,
max_tokens=8096, max_tokens=8096,
@@ -687,10 +685,12 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
"usage": usage_dict, "usage": usage_dict,
}) })
if response.stop_reason != "tool_use": if response.stop_reason != "tool_use" or not tool_use_blocks:
break break
if round_idx >= MAX_TOOL_ROUNDS: if round_idx > MAX_TOOL_ROUNDS:
# The model ignored the MAX ROUNDS warning and kept calling tools.
# Force abort to prevent infinite loop.
break break
tool_results = [] tool_results = []
@@ -728,9 +728,6 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
"content": output, "content": output,
}) })
if not tool_results:
break
# Refresh file context after tool calls and inject into tool result message # Refresh file context after tool calls and inject into tool result message
if file_items: if file_items:
file_items = _reread_file_items(file_items) file_items = _reread_file_items(file_items)
@@ -745,6 +742,12 @@ def _send_anthropic(md_content: str, user_message: str, base_dir: str, file_item
), ),
}) })
if round_idx == MAX_TOOL_ROUNDS:
tool_results.append({
"type": "text",
"text": "SYSTEM WARNING: MAX TOOL ROUNDS REACHED. YOU MUST PROVIDE YOUR FINAL ANSWER NOW WITHOUT CALLING ANY MORE TOOLS."
})
_anthropic_history.append({ _anthropic_history.append({
"role": "user", "role": "user",
"content": tool_results, "content": tool_results,