fix: handle truncated AI responses and relax progressive tree validation
- Strip unclosed [TREE_UPDATE] and [METADATA] blocks from display when response is truncated by max_tokens - Increase send_message max_tokens from 2000 to 8000 to prevent truncation of large tree JSON - Use lightweight validation for progressive tree updates (valid root node only) instead of strict 5-node minimum — strict validation still applies at final /generate step Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -187,6 +187,13 @@ def _parse_ai_response(raw_response: str) -> dict[str, Any]:
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
logger.warning("Failed to parse tree update JSON: %s", e)
|
||||
result["content"] = raw_response[: tree_match.start()] + raw_response[tree_match.end() :]
|
||||
else:
|
||||
# Handle truncated response — opening tag exists but no closing tag
|
||||
# (happens when max_tokens cuts off the JSON block)
|
||||
truncated_match = re.search(r"\[TREE_UPDATE\][\s\S]*$", raw_response)
|
||||
if truncated_match:
|
||||
logger.warning("Truncated [TREE_UPDATE] block detected (no closing tag) — stripping from display")
|
||||
result["content"] = raw_response[: truncated_match.start()]
|
||||
|
||||
# Extract [PHASE:name]
|
||||
phase_match = re.search(r"\[PHASE:(\w+)\]", result["content"])
|
||||
@@ -205,6 +212,11 @@ def _parse_ai_response(raw_response: str) -> dict[str, Any]:
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
logger.warning("Failed to parse metadata JSON: %s", e)
|
||||
result["content"] = result["content"][: meta_match.start()] + result["content"][meta_match.end() :]
|
||||
else:
|
||||
truncated_meta = re.search(r"\[METADATA\][\s\S]*$", result["content"])
|
||||
if truncated_meta:
|
||||
logger.warning("Truncated [METADATA] block detected — stripping from display")
|
||||
result["content"] = result["content"][: truncated_meta.start()]
|
||||
|
||||
# Clean up extra whitespace from marker removal
|
||||
result["content"] = re.sub(r"\n{3,}", "\n\n", result["content"]).strip()
|
||||
@@ -294,18 +306,21 @@ async def send_message(
|
||||
response_text, input_tokens, output_tokens = await provider.generate_text(
|
||||
system_prompt=system_prompt,
|
||||
messages=provider_messages,
|
||||
max_tokens=2000,
|
||||
max_tokens=8000,
|
||||
)
|
||||
|
||||
parsed = _parse_ai_response(response_text)
|
||||
|
||||
# Validate tree update if present
|
||||
# Validate tree update if present (lightweight check for progressive builds —
|
||||
# only require valid root structure, not min node counts)
|
||||
tree_update = parsed["tree_update"]
|
||||
if tree_update:
|
||||
errors = validate_generated_tree(tree_update)
|
||||
if errors:
|
||||
logger.warning("AI tree update failed validation: %s", errors)
|
||||
tree_update = None # Silently discard invalid updates
|
||||
if not isinstance(tree_update, dict) or tree_update.get("type") != "decision":
|
||||
logger.warning("AI tree update rejected: root must be a decision node")
|
||||
tree_update = None
|
||||
elif not tree_update.get("id"):
|
||||
logger.warning("AI tree update rejected: root node missing id")
|
||||
tree_update = None
|
||||
|
||||
# Update session state
|
||||
history.append({"role": "assistant", "content": parsed["content"], "timestamp": now_iso})
|
||||
|
||||
Reference in New Issue
Block a user