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:
chihlasm
2026-02-28 10:38:22 -05:00
parent e79ffff1dc
commit 1f404a20ca

View File

@@ -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})