fix(agent): make preview and browser output parsers linear (#7003)

Six regexes over tool output backtracked polynomially on long whitespace
runs or repeated openers: the browser snapshot generic-line filter and
post-interaction split, the task-row and skill-bullet parsers, the email
"no results" check, and the Google /sorry/ interstitial check.

Rewrite each without overlapping quantifiers, keeping the same matches for
the text these tools emit. Move the /sorry/ check into
is_google_sorry_url() so it can be tested.
This commit is contained in:
nopoz
2026-10-30 22:46:19 -07:00
committed by GitHub
parent a8e0ff1c8e
commit bebf53be3c
3 changed files with 158 additions and 10 deletions
+3 -2
View File
@@ -25,7 +25,7 @@ def compact_browser_observation(value, budget=8000):
# Snapshot text already contains labels and refs in DOM order.
# The refs mapping repeats them and buries menus in raw JSON.
lines = [line for line in snapshot.splitlines()
if not re.fullmatch(r'\s*-?\s*generic(?:\s+\[ref=e\d+\])?:?\s*', line)]
if not re.fullmatch(r'\s*(?:-\s*)?generic(?:\s+\[ref=e\d+\])?:?\s*', line)]
pages.append(('URL: ' + str(url) + '\n' if url else '') + '\n'.join(lines))
elif url:
notices.append('URL: ' + str(url))
@@ -37,7 +37,8 @@ def compact_browser_observation(value, budget=8000):
parsed = json.loads(item)
except (ValueError, TypeError):
# CLI status text precedes the JSON post-interaction state.
parts = re.split(r'\n\n\[(?:post-[^\]]+|page state after failed [^\]]+)\]\n', item)
# Labels are one line; stopping at \n keeps the split linear.
parts = re.split(r'\n\n\[(?:post-[^\]\n]+|page state after failed [^\]\n]+)\]\n', item)
if len(parts) > 1:
for part in parts:
visit(part)
+14 -8
View File
@@ -1229,7 +1229,7 @@ def tasks_terminal_response(raw, *, user_text='', max_items=20):
text = str(payload or '').strip()
rows = []
for line in text.splitlines():
match = re.match(r'^\s*\d+\.\s+(.*?)\s+\(([^)]+)\)\s+—\s+(.+)$', line)
match = re.match(r'^\s*\d+\.\s(.*?)\s\(([^()]+)\)\s+—\s+(.+)$', line)
if match:
details = match[3].strip()
rows.append({
@@ -1346,7 +1346,7 @@ def skills_terminal_response(raw, *, user_text='', max_items=20):
continue
match = re.match(r'^\s*-\s+\*\*([^*]+)\*\*(?:\s+\(([^)]+)\))?', line)
if not match:
match = re.match(r'^\s*-\s+([^:]+?)(?:\s+\(([^)]+)\))?(?:\s*:|$)', line)
match = re.match(r'^\s*-\s+([^\s:][^:]*?)(?:\s\(([^()]+)\)(?:\s*:|$)|:|$)', line)
if match and status:
rows.append((status, match.group(1).strip(), (match.group(2) or '').strip()))
if not rows:
@@ -5065,7 +5065,7 @@ def email_search_result_empty(value):
try:
decoded = json.loads(value)
except (TypeError, ValueError):
return bool(re.fullmatch(r'\s*No emails matched [^\n]+\.?\s*', value, re.I))
return bool(re.fullmatch(r'\s*No emails matched [^\n]+(?:\n\s*)?', value, re.I))
if decoded == value:
return False
value = decoded
@@ -5339,6 +5339,16 @@ def private_browser_effective_url(result):
return urls[-1] if urls else ''
def is_google_sorry_url(url):
"""Recognize Google's rate-limit interstitial."""
# Matching the host separately keeps this linear; one combined pattern
# rescans the host from every dot.
return any(
re.fullmatch(r'(?:[^/]+\.)?google\.[^/]+', host[1], re.I)
for host in re.finditer(r'https?://([^/]+)/sorry/', url, re.I)
)
def browser_observation_page_missing(raw):
"""Recognize a rendered error page, rather than an article mentioning 404."""
text = str(raw or '')
@@ -8213,11 +8223,7 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac
browser_search_url = requested_browser_url or effective_browser_url
is_search_engine_navigation = contains_search_engine_navigation(
browser_search_url
) or bool(re.search(
r'https?://(?:[^/]+\.)?google\.[^/]+/sorry/',
effective_browser_url,
re.I,
))
) or is_google_sorry_url(effective_browser_url)
has_native_search = any(
canonical(schema['function']['name']) == 'web_search'
for schema in offered