v10.13.8: hard tool-strip on budget cap, compaction output stripping, null-tool loop detection
This commit is contained in:
@@ -27,21 +27,14 @@ model_catalog_json = ""
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
("10.13.7", "2026-05-27", [
|
("10.13.8", "2026-05-27", [
|
||||||
|
"Fix: compaction summary strips raw tool outputs (was re-triggering read loops)",
|
||||||
|
"Fix: budget cap now strips tools from request (model literally cannot call tools)",
|
||||||
|
"Fix: detect get_goal/completion_budget null-tool loops (3+ consecutive → force finalize)",
|
||||||
|
"Fix: post-compaction write directive when 10+ reads with 0 writes",
|
||||||
"Fix: strip timestamps from loop hash (<current_date> broke cross-session tracker)",
|
"Fix: strip timestamps from loop hash (<current_date> broke cross-session tracker)",
|
||||||
"Fix: strip base64 image data from tool outputs in normalizer (multi-MB payload bloat)",
|
"Fix: strip base64 image data from tool outputs in normalizer",
|
||||||
"Fix: thread-safe file tracker (was unprotected dict in ThreadingHTTPServer)",
|
"Fix: thread-safe file tracker, response logging for finalize/budget paths",
|
||||||
"Fix: response logging for finalize and budget-cap paths",
|
|
||||||
"Anti-loop: cross-session tracker, tool-call budget (150), file read-loop detection",
|
|
||||||
"Auto 401 token refresh with retry (v2 + OA compat)",
|
|
||||||
"Model-aware idle timeout: flash models 120s, pro 300s",
|
|
||||||
"Smart compaction: directive summary on read-loops",
|
|
||||||
"Default provider policy for unrecognized backends",
|
|
||||||
"Anti-stall fix: no longer kills own parent/process group",
|
|
||||||
"Codex Desktop Updater: check/install/rollback/manual rebuild",
|
|
||||||
"Fix Codex CLI 0.134.0 profiles: separate .config.toml files",
|
|
||||||
"E2E test suite: bash test-antigravity.sh [--task]",
|
|
||||||
"Merge cobra91 PR #17: MSIX Desktop launch, button state",
|
|
||||||
]),
|
]),
|
||||||
("3.12.1", "2026-05-27", [
|
("3.12.1", "2026-05-27", [
|
||||||
"Fix Antigravity adapter (PR #15): simplified model resolution",
|
"Fix Antigravity adapter (PR #15): simplified model resolution",
|
||||||
|
|||||||
@@ -83,21 +83,14 @@ model_catalog_json = ""
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
("10.13.7", "2026-05-27", [
|
("10.13.8", "2026-05-27", [
|
||||||
|
"Fix: compaction summary strips raw tool outputs (was re-triggering read loops)",
|
||||||
|
"Fix: budget cap now strips tools from request (model literally cannot call tools)",
|
||||||
|
"Fix: detect get_goal/completion_budget null-tool loops (3+ consecutive → force finalize)",
|
||||||
|
"Fix: post-compaction write directive when 10+ reads with 0 writes",
|
||||||
"Fix: strip timestamps from loop hash (<current_date> broke cross-session tracker)",
|
"Fix: strip timestamps from loop hash (<current_date> broke cross-session tracker)",
|
||||||
"Fix: strip base64 image data from tool outputs in normalizer (multi-MB payload bloat)",
|
"Fix: strip base64 image data from tool outputs in normalizer",
|
||||||
"Fix: thread-safe file tracker (was unprotected dict in ThreadingHTTPServer)",
|
"Fix: thread-safe file tracker, response logging for finalize/budget paths",
|
||||||
"Fix: response logging for finalize and budget-cap paths",
|
|
||||||
"Anti-loop: cross-session tracker, tool-call budget (150), file read-loop detection",
|
|
||||||
"Auto 401 token refresh with retry",
|
|
||||||
"Model-aware idle timeout: flash 120s, pro 300s",
|
|
||||||
"Smart compaction: directive summary on read-loops",
|
|
||||||
"Default provider policy for unrecognized backends",
|
|
||||||
"Anti-stall fix: no longer kills own parent/process group",
|
|
||||||
"Codex Desktop Updater: check/install/rollback/manual rebuild",
|
|
||||||
"Fix Codex CLI 0.134.0 profiles: separate .config.toml files",
|
|
||||||
"E2E test suite: bash test-antigravity.sh [--task]",
|
|
||||||
"Merge cobra91 PR #17: MSIX Desktop launch, button state",
|
|
||||||
]),
|
]),
|
||||||
("3.12.1", "2026-05-27", [
|
("3.12.1", "2026-05-27", [
|
||||||
"Fix Antigravity adapter (PR #15): simplify model resolution",
|
"Fix Antigravity adapter (PR #15): simplify model resolution",
|
||||||
|
|||||||
@@ -1879,7 +1879,7 @@ def _crof_compact_for_retry(input_data, model, aggression=0):
|
|||||||
return head + tail
|
return head + tail
|
||||||
|
|
||||||
summary_lines = [f"[Auto-compacted: {len(body)} turns removed (adaptive limit={limit})]"]
|
summary_lines = [f"[Auto-compacted: {len(body)} turns removed (adaptive limit={limit})]"]
|
||||||
for item in body[-5:]:
|
for item in body[-3:]:
|
||||||
summary_lines.append(_item_summary(item, max_len=120))
|
summary_lines.append(_item_summary(item, max_len=120))
|
||||||
|
|
||||||
summary_msg = {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "\n".join(summary_lines)}]}
|
summary_msg = {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "\n".join(summary_lines)}]}
|
||||||
@@ -1908,9 +1908,13 @@ def _item_summary(item, max_len=200):
|
|||||||
return f"[tool call] {name}({args[:max_len]})"
|
return f"[tool call] {name}({args[:max_len]})"
|
||||||
elif t == "function_call_output":
|
elif t == "function_call_output":
|
||||||
output = item.get("output", "")
|
output = item.get("output", "")
|
||||||
if len(output) > max_len:
|
out_len = len(output) if isinstance(output, str) else len(str(output))
|
||||||
return f"[tool result] {output[:max_len]}..."
|
exit_match = re.search(r'Process exited with code (\d+)', output if isinstance(output, str) else str(output))
|
||||||
return f"[tool result] {output}"
|
exit_code = exit_match.group(1) if exit_match else "?"
|
||||||
|
tok_match = re.search(r'Original token count: (\d+)', output if isinstance(output, str) else str(output))
|
||||||
|
tok_count = tok_match.group(1) if tok_match else ""
|
||||||
|
tok_info = f" ({tok_count} tokens)" if tok_count else ""
|
||||||
|
return f"[tool result] exit={exit_code}{tok_info} {out_len} chars"
|
||||||
return f"[{t}]"
|
return f"[{t}]"
|
||||||
|
|
||||||
def _extract_files(items):
|
def _extract_files(items):
|
||||||
@@ -1998,6 +2002,10 @@ def _compact_input(input_data):
|
|||||||
tool_summaries.append(_item_summary(item, max_len=150))
|
tool_summaries.append(_item_summary(item, max_len=150))
|
||||||
|
|
||||||
files = _extract_files(body)
|
files = _extract_files(body)
|
||||||
|
n_read_tools = sum(1 for it in body if it.get("type") == "function_call"
|
||||||
|
and any(k in it.get("arguments", "") for k in ["cat ", "head ", "tail ", "sed -n", "grep ", "less ", "more ", "python3 -c", ".read()"]))
|
||||||
|
n_write_tools = sum(1 for it in body if it.get("type") == "function_call"
|
||||||
|
and any(k in it.get("arguments", "") for k in ["write(", ".write", " > ", " >> ", "sed -i", "patch "]))
|
||||||
|
|
||||||
summary_lines = [f"[Auto-compacted: {len(body)} earlier turns summarized to preserve context]"]
|
summary_lines = [f"[Auto-compacted: {len(body)} earlier turns summarized to preserve context]"]
|
||||||
if user_queries:
|
if user_queries:
|
||||||
@@ -2005,11 +2013,13 @@ def _compact_input(input_data):
|
|||||||
if assistant_msgs:
|
if assistant_msgs:
|
||||||
summary_lines.append(f"Assistant responses: {'; '.join(assistant_msgs[-3:])}")
|
summary_lines.append(f"Assistant responses: {'; '.join(assistant_msgs[-3:])}")
|
||||||
if tool_summaries:
|
if tool_summaries:
|
||||||
summary_lines.append(f"Actions taken ({len(tool_summaries)} steps):")
|
summary_lines.append(f"Actions taken ({len(tool_summaries)} steps, {n_read_tools} reads, {n_write_tools} writes):")
|
||||||
for ts in tool_summaries[-15:]:
|
for ts in tool_summaries[-5:]:
|
||||||
summary_lines.append(f" {ts}")
|
summary_lines.append(f" {ts}")
|
||||||
if files:
|
if files:
|
||||||
summary_lines.append(f"Files touched: {', '.join(sorted(files)[-10:])}")
|
summary_lines.append(f"Files touched: {', '.join(sorted(files)[-10:])}")
|
||||||
|
if n_read_tools > 10 and n_write_tools == 0:
|
||||||
|
summary_lines.append("⚠ You have already read these files extensively but made NO edits. You MUST write your changes NOW. Do NOT read any more files.")
|
||||||
|
|
||||||
summary_text = "\n".join(summary_lines)
|
summary_text = "\n".join(summary_lines)
|
||||||
summary_msg = {
|
summary_msg = {
|
||||||
@@ -2135,7 +2145,7 @@ def _adaptive_compact(input_data, model, policy=None):
|
|||||||
if not body:
|
if not body:
|
||||||
return head + tail, True
|
return head + tail, True
|
||||||
summary_lines = [f"[Auto-compacted: {len(body)} turns removed (budget={input_budget}tok, model={model})]"]
|
summary_lines = [f"[Auto-compacted: {len(body)} turns removed (budget={input_budget}tok, model={model})]"]
|
||||||
for item in body[-5:]:
|
for item in body[-3:]:
|
||||||
summary_lines.append(_item_summary(item, max_len=120))
|
summary_lines.append(_item_summary(item, max_len=120))
|
||||||
summary_msg = {"type": "message", "role": "user",
|
summary_msg = {"type": "message", "role": "user",
|
||||||
"content": [{"type": "input_text", "text": "\n".join(summary_lines)}]}
|
"content": [{"type": "input_text", "text": "\n".join(summary_lines)}]}
|
||||||
@@ -5907,6 +5917,18 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
null_tool_names = {"get_goal", "get_remaining_tokens", "get_completion_budget", "status"}
|
||||||
|
consecutive_null = 0
|
||||||
|
for item in reversed(input_data):
|
||||||
|
if isinstance(item, dict):
|
||||||
|
if item.get("type") == "function_call" and item.get("name") in null_tool_names:
|
||||||
|
consecutive_null += 1
|
||||||
|
elif item.get("type") == "function_call":
|
||||||
|
break
|
||||||
|
if consecutive_null >= 3:
|
||||||
|
ag_state["force_finalize"] = True
|
||||||
|
print(f"[{getattr(self, '_session_id', '?')}] [antigravity-loop] NULL-TOOL LOOP: {consecutive_null} consecutive {null_tool_names} calls, forcing finalize", file=sys.stderr)
|
||||||
|
|
||||||
last_tool_key = None
|
last_tool_key = None
|
||||||
for item in reversed(input_data):
|
for item in reversed(input_data):
|
||||||
if isinstance(item, dict) and item.get("type") == "function_call":
|
if isinstance(item, dict) and item.get("type") == "function_call":
|
||||||
@@ -5940,9 +5962,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
request_body["systemInstruction"] = {"role": "user", "parts": system_parts}
|
request_body["systemInstruction"] = {"role": "user", "parts": system_parts}
|
||||||
if gen_config:
|
if gen_config:
|
||||||
request_body["generationConfig"] = gen_config
|
request_body["generationConfig"] = gen_config
|
||||||
if gemini_tools:
|
_budget_exceeded = ag_state.get("total_tool_calls", 0) > _ANTIGRAVITY_MAX_TOOL_CALLS_PER_TASK
|
||||||
|
if gemini_tools and not _budget_exceeded and not ag_state.get("force_finalize"):
|
||||||
request_body["tools"] = gemini_tools
|
request_body["tools"] = gemini_tools
|
||||||
if _is_claude_model and gemini_tools:
|
elif _budget_exceeded or ag_state.get("force_finalize"):
|
||||||
|
print(f"[{getattr(self, '_session_id', '?')}] [antigravity-budget] TOOLS STRIPPED from request (budget exceeded or force_finalize)", file=sys.stderr)
|
||||||
|
if _is_claude_model and "tools" in request_body:
|
||||||
request_body["toolConfig"] = {"functionCallingConfig": {"mode": "VALIDATED"}}
|
request_body["toolConfig"] = {"functionCallingConfig": {"mode": "VALIDATED"}}
|
||||||
|
|
||||||
import platform as _plat
|
import platform as _plat
|
||||||
@@ -6720,22 +6745,34 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
f"{ft['path_counts'][dp]}x, total={ft['total_reads']}", file=sys.stderr)
|
f"{ft['path_counts'][dp]}x, total={ft['total_reads']}", file=sys.stderr)
|
||||||
break
|
break
|
||||||
|
|
||||||
last_tool_key = None
|
null_tool_names = {"get_goal", "get_remaining_tokens", "get_completion_budget", "status"}
|
||||||
for item in reversed(input_data):
|
consecutive_null = 0
|
||||||
if isinstance(item, dict) and item.get("type") == "function_call":
|
for item in reversed(input_data):
|
||||||
fname = item.get("name", "")
|
if isinstance(item, dict):
|
||||||
args_str = json.dumps(item.get("arguments", {}), sort_keys=True)[:100]
|
if item.get("type") == "function_call" and item.get("name") in null_tool_names:
|
||||||
last_tool_key = f"{fname}:{args_str}"
|
consecutive_null += 1
|
||||||
break
|
elif item.get("type") == "function_call":
|
||||||
if last_tool_key:
|
break
|
||||||
if last_tool_key == ag_state["last_tool"]:
|
if consecutive_null >= 3:
|
||||||
ag_state["last_tool_count"] += 1
|
ag_state["force_finalize"] = True
|
||||||
if ag_state["last_tool_count"] >= 5:
|
print(f"[{getattr(self, '_session_id', '?')}] [antigravity-loop] NULL-TOOL LOOP: {consecutive_null} consecutive {null_tool_names} calls, forcing finalize", file=sys.stderr)
|
||||||
ag_state["repeated_tool"] = True
|
|
||||||
ag_state["force_finalize"] = True
|
last_tool_key = None
|
||||||
else:
|
for item in reversed(input_data):
|
||||||
ag_state["last_tool"] = last_tool_key
|
if isinstance(item, dict) and item.get("type") == "function_call":
|
||||||
ag_state["last_tool_count"] = 1
|
fname = item.get("name", "")
|
||||||
|
args_str = json.dumps(item.get("arguments", {}), sort_keys=True)[:100]
|
||||||
|
last_tool_key = f"{fname}:{args_str}"
|
||||||
|
break
|
||||||
|
if last_tool_key:
|
||||||
|
if last_tool_key == ag_state["last_tool"]:
|
||||||
|
ag_state["last_tool_count"] += 1
|
||||||
|
if ag_state["last_tool_count"] >= 5:
|
||||||
|
ag_state["repeated_tool"] = True
|
||||||
|
ag_state["force_finalize"] = True
|
||||||
|
else:
|
||||||
|
ag_state["last_tool"] = last_tool_key
|
||||||
|
ag_state["last_tool_count"] = 1
|
||||||
|
|
||||||
_EDIT_WORDS = ("change", "fix", "update", "redesign", "rewrite", "modify", "improve", "replace", "edit", "make it", "add", "remove", "delete", "rename", "move", "convert")
|
_EDIT_WORDS = ("change", "fix", "update", "redesign", "rewrite", "modify", "improve", "replace", "edit", "make it", "add", "remove", "delete", "rename", "move", "convert")
|
||||||
latest_lower = ""
|
latest_lower = ""
|
||||||
@@ -6789,10 +6826,13 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
request_body["systemInstruction"] = {"parts": system_parts}
|
request_body["systemInstruction"] = {"parts": system_parts}
|
||||||
if gen_config:
|
if gen_config:
|
||||||
request_body["generationConfig"] = gen_config
|
request_body["generationConfig"] = gen_config
|
||||||
if gemini_tools:
|
_budget_exceeded_oa = ag_state.get("total_tool_calls", 0) > _ANTIGRAVITY_MAX_TOOL_CALLS_PER_TASK
|
||||||
|
if gemini_tools and not _budget_exceeded_oa and not ag_state.get("force_finalize"):
|
||||||
request_body["tools"] = gemini_tools
|
request_body["tools"] = gemini_tools
|
||||||
|
elif _budget_exceeded_oa or ag_state.get("force_finalize"):
|
||||||
|
print(f"[antigravity-budget] TOOLS STRIPPED from OA request (budget exceeded or force_finalize)", file=sys.stderr)
|
||||||
|
|
||||||
if OAUTH_PROVIDER == "google-antigravity" and _is_claude_model and gemini_tools:
|
if OAUTH_PROVIDER == "google-antigravity" and _is_claude_model and "tools" in request_body:
|
||||||
request_body["toolConfig"] = {"functionCallingConfig": {"mode": "VALIDATED"}}
|
request_body["toolConfig"] = {"functionCallingConfig": {"mode": "VALIDATED"}}
|
||||||
if _is_claude_thinking:
|
if _is_claude_thinking:
|
||||||
print(f"[antigravity-claude] applied VALIDATED toolConfig for thinking model", file=sys.stderr)
|
print(f"[antigravity-claude] applied VALIDATED toolConfig for thinking model", file=sys.stderr)
|
||||||
|
|||||||
Reference in New Issue
Block a user