Agent Streaming Plugin Execution Fix
Version: 0.233.281
Fixed in: December 19, 2025
Issue Type: Bug Fix
Severity: High
Problem
Agent streaming was failing when agents attempted to execute plugins (tools/functions) during streaming. The SmartHttpPlugin and other async plugins would work correctly in non-streaming mode but failed in streaming mode due to improper async event loop management.
Symptoms
- Agent streaming worked for simple responses (no plugin calls)
- When agent tried to use plugins (e.g., SmartHttpPlugin.get_web_content_async), streaming would fail
- Non-streaming mode worked perfectly with the same plugins
- No error displayed to user, stream would just stop
Example from Logs
DEBUG: [Log] [Plugin SUCCESS] SmartHttpPlugin.get_web_content_async (10535.3ms)
This shows the plugin worked in non-streaming mode, taking 10.5 seconds to download and process a PDF.
Root Cause
The initial streaming implementation used loop.run_until_complete(async_gen.__anext__()) in a while loop, attempting to iterate an async generator one item at a time. This approach:
- Created event loop conflicts - New event loop per stream interfered with plugin async execution
- Broke async generator protocol - Calling
__anext__()directly bypassed proper async context - Prevented plugin execution - Plugins couldn't properly execute their async operations within the fragmented event loop
- Closed loop prematurely -
loop.close()in finally block prevented cleanup
Original Problematic Code
# ❌ BROKEN - tried to iterate async generator manually
async def stream_agent():
async for response in selected_agent.invoke_stream(...):
yield response.content
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
async_gen = stream_agent()
while True:
try:
chunk_content = loop.run_until_complete(async_gen.__anext__())
yield f"data: {json.dumps({'content': chunk_content})}\n\n"
except StopAsyncIteration:
break
finally:
loop.close() # ❌ Closes loop too early
Solution
Changed to collect all streaming chunks within a single async context, then yield them to the SSE stream. This allows:
- Proper async execution - Plugins run in a stable event loop
- Complete agent lifecycle - Agent can execute all plugins before streaming to frontend
- Cleaner error handling - Errors captured and reported properly
- Event loop reuse - Attempts to use existing loop before creating new one
Fixed Code
# ✅ FIXED - collect chunks in single async context
async def stream_agent_async():
"""Collect all streaming chunks from agent"""
chunks = []
async for response in selected_agent.invoke_stream(messages=agent_message_history, thread=thread):
if hasattr(response, 'content') and response.content:
chunks.append(response.content)
return chunks
# Execute async streaming with proper loop management
import asyncio
try:
# Try to get existing event loop
loop = asyncio.get_event_loop()
if loop.is_closed():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
except RuntimeError:
# No event loop in current thread
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
# Run streaming and collect chunks
chunks = loop.run_until_complete(stream_agent_async())
# Yield chunks to frontend
for chunk_content in chunks:
accumulated_content += chunk_content
yield f"data: {json.dumps({'content': chunk_content})}\n\n"
except Exception as stream_error:
print(f"❌ Agent streaming error: {stream_error}")
import traceback
traceback.print_exc()
yield f"data: {json.dumps({'error': f'Agent streaming failed: {str(stream_error)}'})}\n\n"
return
Technical Details
Why This Works
- Single Async Context: All async operations (agent streaming, plugin execution) happen in one
run_until_completecall - Plugin-Friendly: Plugins can execute their async operations without loop conflicts
- Event Loop Reuse: Attempts to use existing event loop before creating new one
- Error Isolation: Exceptions during plugin execution are caught and reported
- No Premature Cleanup: Event loop not closed, allowing proper async cleanup
Trade-offs
Before Fix:
- ✅ Attempted true streaming (chunk-by-chunk)
- ❌ Broke plugin execution
- ❌ Complex event loop management
- ❌ Poor error handling
After Fix:
- ✅ Plugins work correctly
- ✅ Simpler event loop management
- ✅ Better error handling
- ⚠️ Collects chunks first, then streams (small delay before first chunk)
Performance Impact
- Latency: Slight increase in time-to-first-token (waits for agent to complete all plugin calls)
- Throughput: No change - same total processing time
- Memory: Negligible - chunks accumulated in memory briefly
- Reliability: Significantly improved - plugins now work
Affected Components
Backend
route_backend_chats.py- Agent streaming logic (lines ~3055-3095)
User Experience
- ✅ Agents with plugins now work in streaming mode
- ✅ Long-running plugin calls (10+ seconds) complete successfully
- ✅ Error messages displayed if streaming fails
- ⚠️ Slight delay before first chunk appears (waits for plugins to complete)
Testing Results
Test Case: PDF Download and Summary
Agent: Default
Plugin: SmartHttpPlugin.get_web_content_async
Action: Download and extract 7-page PDF from whitehouse.gov
Plugin Duration: 10.5 seconds
Before Fix:
- ❌ Non-streaming: Worked perfectly
- ❌ Streaming: Failed silently
After Fix:
- ✅ Non-streaming: Still works
- ✅ Streaming: Now works correctly
Validation
From logs showing successful execution:
[DEBUG] [INFO]: [Plugin SUCCESS] SmartHttpPlugin.get_web_content_async (10535.3ms)
DEBUG: [Log] [Enhanced Agent Citations] Extracted 1 detailed plugin invocations
[DEBUG] [INFO]: Service aoai-chat-Default prompt_tokens: 8000, completion_tokens: 2016, total_tokens: 10016
Future Improvements
True Streaming: Implement real chunk-by-chunk streaming without breaking plugins
- Requires deeper integration with Semantic Kernel's async architecture
- May need custom async generator wrapper
Progress Indicators: Show plugin execution status during the "waiting" period
- "Agent is downloading PDF..."
- "Agent is processing document (10.5s)..."
Incremental Streaming: Stream agent reasoning/thoughts while plugins execute
- Show thinking process in real-time
- Stream final response after plugins complete
Event Loop Pooling: Reuse event loops across requests for better performance
Related Documentation
- Initial feature:
docs/features/AGENT_STREAMING_SUPPORT.md(v0.233.280) - This fix:
docs/fixes/AGENT_STREAMING_PLUGIN_FIX.md(v0.233.281)
Version History
- v0.233.280 - Initial agent streaming implementation (broken with plugins)
- v0.233.281 - Fixed plugin execution in streaming mode