@@ -657,6 +657,63 @@ def all_patterns(self) -> list[str]:
657657# ─────────────────────────────────────────────────────────────────────────────
658658
659659
660+ @dataclass
661+ class IncrementalJsonState :
662+ """State for incremental JSON parsing (O(delta) per token instead of O(content))."""
663+
664+ open_braces : int = 0
665+ close_braces : int = 0
666+ open_brackets : int = 0
667+ close_brackets : int = 0
668+ in_string : bool = False
669+ escape_next : bool = False
670+ processed_length : int = 0
671+
672+
673+ def update_json_state_incremental (
674+ state : IncrementalJsonState ,
675+ delta : str ,
676+ ) -> IncrementalJsonState :
677+ """Update JSON state incrementally with new delta content.
678+
679+ Only processes the delta, not the full content - O(delta) per call.
680+
681+ Args:
682+ state: Current incremental state
683+ delta: New content to process
684+
685+ Returns:
686+ Updated state (mutates and returns the same object)
687+ """
688+ for char in delta :
689+ if state .escape_next :
690+ state .escape_next = False
691+ continue
692+
693+ if char == "\\ " and state .in_string :
694+ state .escape_next = True
695+ continue
696+
697+ if char == '"' and not state .escape_next :
698+ state .in_string = not state .in_string
699+ continue
700+
701+ if state .in_string :
702+ continue
703+
704+ if char == "{" :
705+ state .open_braces += 1
706+ elif char == "}" :
707+ state .close_braces += 1
708+ elif char == "[" :
709+ state .open_brackets += 1
710+ elif char == "]" :
711+ state .close_brackets += 1
712+
713+ state .processed_length += len (delta )
714+ return state
715+
716+
660717@dataclass
661718class JsonAnalysis :
662719 """Result of JSON structure analysis."""
@@ -1493,24 +1550,52 @@ def json_rule() -> GuardrailRule:
14931550 - Unclosed strings
14941551 - Multiple consecutive commas
14951552 - Malformed patterns like {, or [,
1553+
1554+ Uses incremental state tracking for O(delta) per-token updates instead of
1555+ O(content) full scans during streaming. Only does full analysis at completion.
1556+
1557+ Note: State is reset when content is empty or shorter than processed length
1558+ to handle new streams, aborted streams, or rule reuse.
14961559 """
1560+ # Incremental state for O(delta) streaming checks
1561+ incremental_state = IncrementalJsonState ()
1562+ last_content_length = 0
14971563
14981564 def check (state : State ) -> list [GuardrailViolation ]:
1565+ nonlocal incremental_state , last_content_length
1566+
14991567 content = state .content
15001568 if not content .strip ():
1569+ # Reset state when content is empty (new stream starting)
1570+ incremental_state = IncrementalJsonState ()
1571+ last_content_length = 0
15011572 return []
15021573
15031574 # Only check if it looks like JSON
15041575 if not looks_like_json (content ):
1576+ # Reset state when content doesn't look like JSON
1577+ incremental_state = IncrementalJsonState ()
1578+ last_content_length = 0
15051579 return []
15061580
1507- analysis = analyze_json_structure (content )
1581+ # Reset state if content is shorter than what we've processed
1582+ # (indicates a new stream or aborted stream being reused)
1583+ if len (content ) < last_content_length :
1584+ incremental_state = IncrementalJsonState ()
1585+ last_content_length = 0
1586+
15081587 violations = []
15091588
1510- # During streaming, only report critical issues
1589+ # During streaming, use incremental state tracking (O(delta) instead of O(content))
15111590 if not state .completed :
1512- # Too many closes is always bad
1513- if analysis .close_braces > analysis .open_braces :
1591+ # Get delta since last check
1592+ if len (content ) > last_content_length :
1593+ delta = content [last_content_length :]
1594+ update_json_state_incremental (incremental_state , delta )
1595+ last_content_length = len (content )
1596+
1597+ # Check for critical issues using incremental state
1598+ if incremental_state .close_braces > incremental_state .open_braces :
15141599 violations .append (
15151600 GuardrailViolation (
15161601 rule = "json" ,
@@ -1519,7 +1604,7 @@ def check(state: State) -> list[GuardrailViolation]:
15191604 suggestion = "Check JSON structure" ,
15201605 )
15211606 )
1522- if analysis .close_brackets > analysis .open_brackets :
1607+ if incremental_state .close_brackets > incremental_state .open_brackets :
15231608 violations .append (
15241609 GuardrailViolation (
15251610 rule = "json" ,
@@ -1528,18 +1613,11 @@ def check(state: State) -> list[GuardrailViolation]:
15281613 suggestion = "Check JSON structure" ,
15291614 )
15301615 )
1531- # Report malformed patterns immediately
1532- for issue in analysis .issues :
1533- if "Malformed pattern" in issue or "consecutive commas" in issue :
1534- violations .append (
1535- GuardrailViolation (
1536- rule = "json" ,
1537- message = issue ,
1538- severity = "error" ,
1539- )
1540- )
15411616 else :
1542- # On completion, check for both extra closes AND missing closes
1617+ # On completion, do full analysis for comprehensive check
1618+ analysis = analyze_json_structure (content )
1619+
1620+ # Check for both extra closes AND missing closes
15431621 if analysis .close_braces > analysis .open_braces :
15441622 violations .append (
15451623 GuardrailViolation (
@@ -1583,6 +1661,10 @@ def check(state: State) -> list[GuardrailViolation]:
15831661 )
15841662 )
15851663
1664+ # Reset incremental state for potential reuse
1665+ incremental_state = IncrementalJsonState ()
1666+ last_content_length = 0
1667+
15861668 return violations
15871669
15881670 return GuardrailRule (
0 commit comments