Pure streaming filter that removes ~~~tool_call ... ~~~ and
~~~tool_result ... ~~~ fenced blocks from chunked assistant text.
Non-native-tool-call providers emit tool calls as text fences (the
ExAthena.ToolCalls.TextTagged protocol, ~~~tool_call\s*\n ... \n~~~).
Some models additionally roleplay the result side, echoing
~~~tool_result fences (often with invented tool output) into their
visible text. When this output streams to hosts as {:content, chunk}
deltas, the raw fence markup leaks into the transcript — the loop already
surfaces real tool activity as {:tool_call, ...} / {:tool_result, ...}
events, so both fence kinds must be stripped from the text stream.
Chunks may split a marker at any byte boundary, so the filter holds the longest chunk tail that could still become a marker as pending text:
- Outside a fence, a tail like
"~~","~~~tool_"(shared by both markers),"~~~tool_c"(only~~~tool_call), or"~~~tool_r"(only~~~tool_result) — any strict prefix of either open marker — is held instead of emitted. A complete~~~tool_call/~~~tool_resultis also held while only optional whitespace follows — the open marker is<marker>\s*\n(mirroring the TextTagged regex), so until the newline arrives we can't tell a fence from literal text like~~~tool_caller. If a non-whitespace char arrives first, the held text is released as visible. - Inside a fence, everything is swallowed until the close marker
\n~~~; a tail like"\n~"is held the same way.
flush/1 ends the stream: pending text held outside a fence turned out
to be real text and is returned; an unterminated fence is swallowed
entirely (half a fence is still tool markup, and showing it is worse).
Summary
Functions
End of stream. Releases pending text that never completed into an open marker; swallows the contents of an unterminated fence.
Create a fresh filter state (outside any fence, nothing pending).
Feed a chunk through the filter. Returns {new_state, visible} where
visible is the text safe to show (may be "" while inside a fence or
while holding a possible partial marker).
Types
Functions
End of stream. Releases pending text that never completed into an open marker; swallows the contents of an unterminated fence.
@spec new() :: state()
Create a fresh filter state (outside any fence, nothing pending).
Feed a chunk through the filter. Returns {new_state, visible} where
visible is the text safe to show (may be "" while inside a fence or
while holding a possible partial marker).