Spaces:
Paused
fix(antigravity): π improve function call response pairing with recovery strategies
Browse filesEnhanced the response grouping logic in AntigravityProvider to handle ID mismatches between function calls and their responses more robustly.
- Added three-tier matching strategy: direct ID match, function name match, then order-based fallback
- Function names are now tracked alongside IDs for orphan response recovery
- Responses with "unknown_function" can now be repaired with correct function names
- Placeholder responses are automatically created for completely missing tool responses
- Fixed insertion position tracking to ensure responses are added immediately after their corresponding model message
- Pending groups are now processed in reverse order to prevent index shifting during insertion
- Re-enabled debug logging for response collection and group satisfaction
- Added comprehensive recovery logging for troubleshooting pairing issues
This prevents conversation history corruption when client/proxy systems mutate response IDs or when responses are lost during context processing.
|
@@ -2160,9 +2160,18 @@ class AntigravityProvider(AntigravityAuthBase, ProviderInterface):
|
|
| 2160 |
to grouped format (model with calls, user with all responses).
|
| 2161 |
|
| 2162 |
IMPORTANT: Preserves ID-based pairing to prevent mismatches.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2163 |
"""
|
| 2164 |
new_contents = []
|
| 2165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2166 |
collected_responses = {} # Dict mapping ID -> response_part
|
| 2167 |
|
| 2168 |
for content in contents:
|
|
@@ -2182,7 +2191,9 @@ class AntigravityProvider(AntigravityAuthBase, ProviderInterface):
|
|
| 2182 |
f"Ignoring duplicate - this may indicate malformed conversation history."
|
| 2183 |
)
|
| 2184 |
continue
|
| 2185 |
-
|
|
|
|
|
|
|
| 2186 |
collected_responses[resp_id] = resp
|
| 2187 |
|
| 2188 |
# Try to satisfy pending groups (newest first)
|
|
@@ -2197,10 +2208,10 @@ class AntigravityProvider(AntigravityAuthBase, ProviderInterface):
|
|
| 2197 |
collected_responses.pop(gid) for gid in group_ids
|
| 2198 |
]
|
| 2199 |
new_contents.append({"parts": group_responses, "role": "user"})
|
| 2200 |
-
|
| 2201 |
-
|
| 2202 |
-
|
| 2203 |
-
|
| 2204 |
pending_groups.pop(i)
|
| 2205 |
break
|
| 2206 |
continue
|
|
@@ -2213,14 +2224,22 @@ class AntigravityProvider(AntigravityAuthBase, ProviderInterface):
|
|
| 2213 |
fc.get("functionCall", {}).get("id", "") for fc in func_calls
|
| 2214 |
]
|
| 2215 |
call_ids = [cid for cid in call_ids if cid] # Filter empty IDs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2216 |
if call_ids:
|
| 2217 |
lib_logger.debug(
|
| 2218 |
-
f"[Grouping] Created pending group expecting {len(call_ids)} responses:
|
|
|
|
| 2219 |
)
|
| 2220 |
pending_groups.append(
|
| 2221 |
{
|
| 2222 |
"ids": call_ids,
|
| 2223 |
-
"
|
|
|
|
| 2224 |
}
|
| 2225 |
)
|
| 2226 |
else:
|
|
@@ -2228,37 +2247,120 @@ class AntigravityProvider(AntigravityAuthBase, ProviderInterface):
|
|
| 2228 |
|
| 2229 |
# Handle remaining groups (shouldn't happen in well-formed conversations)
|
| 2230 |
# Attempt recovery by matching orphans to unsatisfied calls
|
|
|
|
|
|
|
|
|
|
| 2231 |
for group in pending_groups:
|
| 2232 |
group_ids = group["ids"]
|
|
|
|
|
|
|
| 2233 |
group_responses = []
|
| 2234 |
|
| 2235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2236 |
if expected_id in collected_responses:
|
|
|
|
| 2237 |
group_responses.append(collected_responses.pop(expected_id))
|
|
|
|
|
|
|
|
|
|
| 2238 |
elif collected_responses:
|
| 2239 |
-
#
|
| 2240 |
-
|
| 2241 |
-
# Get the first available orphan ID to maintain order
|
| 2242 |
-
orphan_id = next(iter(collected_responses))
|
| 2243 |
-
orphan_resp = collected_responses.pop(orphan_id)
|
| 2244 |
|
| 2245 |
-
#
|
| 2246 |
-
orphan_resp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2247 |
|
| 2248 |
-
|
| 2249 |
-
|
| 2250 |
-
|
| 2251 |
-
|
| 2252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2253 |
|
| 2254 |
-
|
| 2255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2256 |
|
| 2257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2258 |
lib_logger.warning(
|
| 2259 |
-
f"[Grouping]
|
| 2260 |
-
f"
|
| 2261 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2262 |
|
| 2263 |
# Warn about unmatched responses
|
| 2264 |
if collected_responses:
|
|
|
|
| 2160 |
to grouped format (model with calls, user with all responses).
|
| 2161 |
|
| 2162 |
IMPORTANT: Preserves ID-based pairing to prevent mismatches.
|
| 2163 |
+
When IDs don't match, attempts recovery by:
|
| 2164 |
+
1. Matching by function name first
|
| 2165 |
+
2. Matching by order if names don't match
|
| 2166 |
+
3. Inserting placeholder responses if responses are missing
|
| 2167 |
+
4. Inserting responses at the CORRECT position (after their corresponding call)
|
| 2168 |
"""
|
| 2169 |
new_contents = []
|
| 2170 |
+
# Each pending group tracks:
|
| 2171 |
+
# - ids: expected response IDs
|
| 2172 |
+
# - func_names: expected function names (for orphan matching)
|
| 2173 |
+
# - insert_after_idx: position in new_contents where model message was added
|
| 2174 |
+
pending_groups = []
|
| 2175 |
collected_responses = {} # Dict mapping ID -> response_part
|
| 2176 |
|
| 2177 |
for content in contents:
|
|
|
|
| 2191 |
f"Ignoring duplicate - this may indicate malformed conversation history."
|
| 2192 |
)
|
| 2193 |
continue
|
| 2194 |
+
lib_logger.debug(
|
| 2195 |
+
f"[Grouping] Collected response for ID: {resp_id}"
|
| 2196 |
+
)
|
| 2197 |
collected_responses[resp_id] = resp
|
| 2198 |
|
| 2199 |
# Try to satisfy pending groups (newest first)
|
|
|
|
| 2208 |
collected_responses.pop(gid) for gid in group_ids
|
| 2209 |
]
|
| 2210 |
new_contents.append({"parts": group_responses, "role": "user"})
|
| 2211 |
+
lib_logger.debug(
|
| 2212 |
+
f"[Grouping] Satisfied group with {len(group_responses)} responses: "
|
| 2213 |
+
f"ids={group_ids}"
|
| 2214 |
+
)
|
| 2215 |
pending_groups.pop(i)
|
| 2216 |
break
|
| 2217 |
continue
|
|
|
|
| 2224 |
fc.get("functionCall", {}).get("id", "") for fc in func_calls
|
| 2225 |
]
|
| 2226 |
call_ids = [cid for cid in call_ids if cid] # Filter empty IDs
|
| 2227 |
+
|
| 2228 |
+
# Also extract function names for orphan matching
|
| 2229 |
+
func_names = [
|
| 2230 |
+
fc.get("functionCall", {}).get("name", "") for fc in func_calls
|
| 2231 |
+
]
|
| 2232 |
+
|
| 2233 |
if call_ids:
|
| 2234 |
lib_logger.debug(
|
| 2235 |
+
f"[Grouping] Created pending group expecting {len(call_ids)} responses: "
|
| 2236 |
+
f"ids={call_ids}, names={func_names}"
|
| 2237 |
)
|
| 2238 |
pending_groups.append(
|
| 2239 |
{
|
| 2240 |
"ids": call_ids,
|
| 2241 |
+
"func_names": func_names,
|
| 2242 |
+
"insert_after_idx": len(new_contents) - 1,
|
| 2243 |
}
|
| 2244 |
)
|
| 2245 |
else:
|
|
|
|
| 2247 |
|
| 2248 |
# Handle remaining groups (shouldn't happen in well-formed conversations)
|
| 2249 |
# Attempt recovery by matching orphans to unsatisfied calls
|
| 2250 |
+
# Process in REVERSE order of insert_after_idx so insertions don't shift indices
|
| 2251 |
+
pending_groups.sort(key=lambda g: g["insert_after_idx"], reverse=True)
|
| 2252 |
+
|
| 2253 |
for group in pending_groups:
|
| 2254 |
group_ids = group["ids"]
|
| 2255 |
+
group_func_names = group.get("func_names", [])
|
| 2256 |
+
insert_idx = group["insert_after_idx"] + 1
|
| 2257 |
group_responses = []
|
| 2258 |
|
| 2259 |
+
lib_logger.debug(
|
| 2260 |
+
f"[Grouping Recovery] Processing unsatisfied group: "
|
| 2261 |
+
f"ids={group_ids}, names={group_func_names}, insert_at={insert_idx}"
|
| 2262 |
+
)
|
| 2263 |
+
|
| 2264 |
+
for i, expected_id in enumerate(group_ids):
|
| 2265 |
+
expected_name = group_func_names[i] if i < len(group_func_names) else ""
|
| 2266 |
+
|
| 2267 |
if expected_id in collected_responses:
|
| 2268 |
+
# Direct ID match
|
| 2269 |
group_responses.append(collected_responses.pop(expected_id))
|
| 2270 |
+
lib_logger.debug(
|
| 2271 |
+
f"[Grouping Recovery] Direct ID match for '{expected_id}'"
|
| 2272 |
+
)
|
| 2273 |
elif collected_responses:
|
| 2274 |
+
# Try to find orphan with matching function name first
|
| 2275 |
+
matched_orphan_id = None
|
|
|
|
|
|
|
|
|
|
| 2276 |
|
| 2277 |
+
# First pass: match by function name
|
| 2278 |
+
for orphan_id, orphan_resp in collected_responses.items():
|
| 2279 |
+
orphan_name = orphan_resp.get("functionResponse", {}).get(
|
| 2280 |
+
"name", ""
|
| 2281 |
+
)
|
| 2282 |
+
# Match if names are equal, or if orphan has "unknown_function" (can be fixed)
|
| 2283 |
+
if orphan_name == expected_name:
|
| 2284 |
+
matched_orphan_id = orphan_id
|
| 2285 |
+
lib_logger.debug(
|
| 2286 |
+
f"[Grouping Recovery] Matched orphan '{orphan_id}' by name '{orphan_name}'"
|
| 2287 |
+
)
|
| 2288 |
+
break
|
| 2289 |
|
| 2290 |
+
# Second pass: if no name match, try "unknown_function" orphans
|
| 2291 |
+
if not matched_orphan_id:
|
| 2292 |
+
for orphan_id, orphan_resp in collected_responses.items():
|
| 2293 |
+
orphan_name = orphan_resp.get("functionResponse", {}).get(
|
| 2294 |
+
"name", ""
|
| 2295 |
+
)
|
| 2296 |
+
if orphan_name == "unknown_function":
|
| 2297 |
+
matched_orphan_id = orphan_id
|
| 2298 |
+
lib_logger.debug(
|
| 2299 |
+
f"[Grouping Recovery] Matched unknown_function orphan '{orphan_id}' "
|
| 2300 |
+
f"to expected '{expected_name}'"
|
| 2301 |
+
)
|
| 2302 |
+
break
|
| 2303 |
+
|
| 2304 |
+
# Third pass: if still no match, take first available (order-based)
|
| 2305 |
+
if not matched_orphan_id:
|
| 2306 |
+
matched_orphan_id = next(iter(collected_responses))
|
| 2307 |
+
lib_logger.debug(
|
| 2308 |
+
f"[Grouping Recovery] No name match, using first available orphan '{matched_orphan_id}'"
|
| 2309 |
+
)
|
| 2310 |
|
| 2311 |
+
if matched_orphan_id:
|
| 2312 |
+
orphan_resp = collected_responses.pop(matched_orphan_id)
|
| 2313 |
+
|
| 2314 |
+
# Fix the ID in the response to match the call
|
| 2315 |
+
old_id = orphan_resp["functionResponse"].get("id", "")
|
| 2316 |
+
orphan_resp["functionResponse"]["id"] = expected_id
|
| 2317 |
|
| 2318 |
+
# Fix the name if it was "unknown_function"
|
| 2319 |
+
if (
|
| 2320 |
+
orphan_resp["functionResponse"].get("name")
|
| 2321 |
+
== "unknown_function"
|
| 2322 |
+
and expected_name
|
| 2323 |
+
):
|
| 2324 |
+
orphan_resp["functionResponse"]["name"] = expected_name
|
| 2325 |
+
lib_logger.info(
|
| 2326 |
+
f"[Grouping Recovery] Fixed function name from 'unknown_function' to '{expected_name}'"
|
| 2327 |
+
)
|
| 2328 |
+
|
| 2329 |
+
lib_logger.warning(
|
| 2330 |
+
f"[Grouping] Auto-repaired ID mismatch: mapped response '{old_id}' "
|
| 2331 |
+
f"to call '{expected_id}' (function: {expected_name})"
|
| 2332 |
+
)
|
| 2333 |
+
group_responses.append(orphan_resp)
|
| 2334 |
+
else:
|
| 2335 |
+
# No responses available - create placeholder
|
| 2336 |
+
placeholder_resp = {
|
| 2337 |
+
"functionResponse": {
|
| 2338 |
+
"name": expected_name or "unknown_function",
|
| 2339 |
+
"response": {
|
| 2340 |
+
"result": {
|
| 2341 |
+
"error": "Tool response was lost during context processing. "
|
| 2342 |
+
"This is a recovered placeholder.",
|
| 2343 |
+
"recovered": True,
|
| 2344 |
+
}
|
| 2345 |
+
},
|
| 2346 |
+
"id": expected_id,
|
| 2347 |
+
}
|
| 2348 |
+
}
|
| 2349 |
lib_logger.warning(
|
| 2350 |
+
f"[Grouping Recovery] Created placeholder response for missing tool: "
|
| 2351 |
+
f"id='{expected_id}', name='{expected_name}'"
|
| 2352 |
)
|
| 2353 |
+
group_responses.append(placeholder_resp)
|
| 2354 |
+
|
| 2355 |
+
if group_responses:
|
| 2356 |
+
# Insert at the correct position (right after the model message with the calls)
|
| 2357 |
+
new_contents.insert(
|
| 2358 |
+
insert_idx, {"parts": group_responses, "role": "user"}
|
| 2359 |
+
)
|
| 2360 |
+
lib_logger.info(
|
| 2361 |
+
f"[Grouping Recovery] Inserted {len(group_responses)} responses at position {insert_idx} "
|
| 2362 |
+
f"(expected {len(group_ids)})"
|
| 2363 |
+
)
|
| 2364 |
|
| 2365 |
# Warn about unmatched responses
|
| 2366 |
if collected_responses:
|