|
25 | 25 | set_rotation_callbacks, model_fallback_manager |
26 | 26 | ) |
27 | 27 | from .tools import TOOL_DEFINITIONS, execute_tool, TOOLS, get_all_tool_definitions |
| 28 | +from .terminal import terminal_title |
28 | 29 | from .ui import ( |
29 | 30 | console, display_tool_call, display_tool_result, display_executed_tool, |
30 | 31 | display_code_execution_result, display_info, display_warning, |
@@ -397,6 +398,7 @@ def _generate(): |
397 | 398 | try: |
398 | 399 | title = self.client_manager.generate_title(first_message) |
399 | 400 | history_manager.set_title(title) |
| 401 | + terminal_title.set_session(title) |
400 | 402 | log_debug(f"Generated title: {title}") |
401 | 403 | except Exception as e: |
402 | 404 | log_error("Failed to generate title", e) |
@@ -435,18 +437,91 @@ def _parse_tool_calls_from_text(self, text: str) -> List[ToolCall]: |
435 | 437 |
|
436 | 438 | return tool_calls |
437 | 439 |
|
| 440 | + def _repair_json(self, json_str: str) -> str: |
| 441 | + """Attempt to repair malformed JSON from LLM responses""" |
| 442 | + if not json_str or not json_str.strip(): |
| 443 | + return "{}" |
| 444 | + |
| 445 | + s = json_str.strip() |
| 446 | + |
| 447 | + # Extract JSON if wrapped in markdown code blocks |
| 448 | + if "```json" in s: |
| 449 | + start = s.find("```json") + 7 |
| 450 | + end = s.find("```", start) |
| 451 | + if end > start: |
| 452 | + s = s[start:end].strip() |
| 453 | + elif "```" in s: |
| 454 | + start = s.find("```") + 3 |
| 455 | + end = s.find("```", start) |
| 456 | + if end > start: |
| 457 | + s = s[start:end].strip() |
| 458 | + |
| 459 | + # Find the actual JSON object/array |
| 460 | + first_brace = s.find('{') |
| 461 | + first_bracket = s.find('[') |
| 462 | + if first_brace == -1 and first_bracket == -1: |
| 463 | + return "{}" |
| 464 | + |
| 465 | + if first_brace != -1 and (first_bracket == -1 or first_brace < first_bracket): |
| 466 | + s = s[first_brace:] |
| 467 | + # Find matching closing brace |
| 468 | + depth = 0 |
| 469 | + in_string = False |
| 470 | + escape = False |
| 471 | + end_idx = len(s) |
| 472 | + for i, c in enumerate(s): |
| 473 | + if escape: |
| 474 | + escape = False |
| 475 | + continue |
| 476 | + if c == '\\': |
| 477 | + escape = True |
| 478 | + continue |
| 479 | + if c == '"' and not escape: |
| 480 | + in_string = not in_string |
| 481 | + continue |
| 482 | + if in_string: |
| 483 | + continue |
| 484 | + if c == '{': |
| 485 | + depth += 1 |
| 486 | + elif c == '}': |
| 487 | + depth -= 1 |
| 488 | + if depth == 0: |
| 489 | + end_idx = i + 1 |
| 490 | + break |
| 491 | + s = s[:end_idx] |
| 492 | + elif first_bracket != -1: |
| 493 | + s = s[first_bracket:] |
| 494 | + |
| 495 | + # Fix common JSON issues |
| 496 | + # Remove trailing commas before } or ] |
| 497 | + s = re.sub(r',\s*([}\]])', r'\1', s) |
| 498 | + |
| 499 | + # Balance braces if needed |
| 500 | + open_braces = s.count('{') - s.count('}') |
| 501 | + open_brackets = s.count('[') - s.count(']') |
| 502 | + s = s + '}' * max(0, open_braces) + ']' * max(0, open_brackets) |
| 503 | + |
| 504 | + return s |
| 505 | + |
438 | 506 | def _parse_tool_args(self, tc) -> dict: |
439 | | - """Parse tool call arguments""" |
| 507 | + """Parse tool call arguments with JSON repair for malformed responses""" |
440 | 508 | try: |
441 | 509 | if isinstance(tc.arguments, dict): |
442 | 510 | return tc.arguments |
443 | 511 | elif isinstance(tc.arguments, str) and tc.arguments.strip(): |
444 | 512 | return json.loads(tc.arguments) |
445 | 513 | else: |
446 | 514 | return {} |
447 | | - except json.JSONDecodeError as e: |
448 | | - log_error("Failed to parse tool arguments", e, {"tool": tc.name, "args": tc.arguments[:200] if tc.arguments else ""}) |
449 | | - return {} |
| 515 | + except json.JSONDecodeError: |
| 516 | + # Try to repair the JSON |
| 517 | + try: |
| 518 | + repaired = self._repair_json(tc.arguments) |
| 519 | + result = json.loads(repaired) |
| 520 | + log_debug(f"Repaired malformed JSON for tool {tc.name}") |
| 521 | + return result |
| 522 | + except json.JSONDecodeError as e: |
| 523 | + log_error("Failed to parse tool arguments", e, {"tool": tc.name, "args": tc.arguments[:200] if tc.arguments else ""}) |
| 524 | + return {} |
450 | 525 |
|
451 | 526 | def _execute_tool_only(self, tc, args: dict) -> tuple: |
452 | 527 | """Execute a tool without UI - for parallel execution""" |
|
0 commit comments