How to Troubleshoot Agent Execution and Streaming Responses in AI Hedge Fund
To troubleshoot agent execution and streaming responses in the AI Hedge Fund project, verify that agents return properly structured dictionaries with valid action strings and numeric quantities, ensure the AgentProgress handler is registered in the event generator to propagate updates through the asyncio queue, and inspect the FastAPI streaming logic for proper task lifecycle management and client-disconnect handling.
The virattt/ai-hedge-fund repository implements a real-time backtesting engine that streams agent decisions via Server-Sent Events (SSE). When agents execute silently or streams hang without emitting data, the issue typically lies in the normalization pipeline, the progress tracking system, or the async generator lifecycle. Understanding the data flow from agent output to SSE client is essential for effective debugging.
Understanding the Streaming Architecture
The application routes all streaming data through a specific pipeline:
Agent → AgentProgress.update_status → progress.register_handler → asyncio.Queue → FastAPI StreamingResponse → SSE client
If any link in this chain fails, the client sees a stalled or empty stream, and the back-test may silently skip days. The major components include:
- FastAPI endpoints (
/hedge-fund/runand/hedge-fund/backtest) inapp/backend/routes/hedge_fund.pythat produce SSE streams and handle client disconnections - Progress tracker (
src/utils/progress.py) that collects status updates via the globalAgentProgressinstance - Agent controller (
src/backtesting/controller.py) that normalizes raw agent output into cleandecisionsmaps - Trade executor (
src/backtesting/trader.py) that converts normalized decisions into portfolio changes - Back-test engine (
src/backtesting/engine.py) that drives the daily loop and pushes progress events
Troubleshooting Agent Execution
When an agent appears to run but produces no trades or decisions, trace through these validation layers:
Validate the Agent Signature
The agent must be a callable that accepts the exact parameters defined in AgentController.run_agent: tickers, start_date, end_date, portfolio, model_name, model_provider, and selected_analysts. Missing arguments or mismatched return types raise exceptions before any progress events are emitted.
Check the Normalization Step
In src/backtesting/controller.py, the run_agent method coerces the action field to the Action enum and quantity to float. If an agent returns a non-dict or uses unexpected keys, the controller falls back to "hold" and 0. Inspect the loop that builds normalized_decisions (lines 44-58)—exceptions here are silently swallowed, so enable logging inside the except blocks to see why a decision was ignored.
Inspect Trade Execution
The execute_trade method in src/backtesting/trader.py applies additional filters:
- Quantities less than or equal to 0 are immediately ignored (line 18)
- Action strings are converted to the
Actionenum (lines 22-25) - If enum conversion fails, the trade defaults to hold
Common Failure Patterns
- Invalid action name: Using
"Buy"instead of"buy"causes the controller to fall back toHOLD - Quantity as string: While the controller casts to
float, a non-numeric string results in0.0 - Missing price data: The back-test engine skips days when data-fetch helpers return empty DataFrames (lines 27-31 in
engine.py)
Add temporary logging inside run_agent and execute_trade to surface raw agent output before normalization occurs.
Debugging Streaming Responses
When the endpoint returns 200 OK but the stream contains only the initial StartEvent or hangs indefinitely, investigate these specific areas:
Confirm SSE Endpoint Registration
Verify that app/main.py properly imports and includes the router containing /hedge-fund/run and /hedge-fund/backtest. A 404 error indicates the FastAPI app isn't registering the routes from app/backend/routes/hedge_fund.py.
Event Generator Lifecycle
The async generator in hedge_fund.py starts by registering a progress handler via progress.register_handler and launching the core task (run_graph_async or BacktestService.run_backtest_async). If the handler registration at line 75 fails to execute, no events reach the queue, resulting in a silent stream that returns only headers.
Progress Queue Handling
The generator awaits progress_queue.get() with a 1-second timeout (lines 12-15). If AgentProgress.update_status is never called for an agent, the queue blocks until timeout. Verify that update_status forwards updates to all registered handlers (line 61 in src/utils/progress.py).
Client Disconnect Detection
The wait_for_disconnect task polls await request.receive() and cancels the background task if it detects "http.disconnect" (lines 53-58). If the client disconnects early, the task cancellation triggers the finally block (lines 42-53), which removes the handler and cancels the background task again. Stray exceptions escaping this block can terminate the response prematurely without sending the final CompleteEvent.
SSE Formatting Validation
Events are serialized via .to_sse(). If this method in app/backend/models/events.py produces malformed SSE (missing the \n\n delimiter), browsers will never fire the message event. Ensure the formatting follows the SSE specification precisely.
Practical Debugging Examples
Use these snippets to isolate issues in your agent execution and streaming pipeline.
Minimal Agent Implementation
Create a simple agent that always buys 10 shares to verify the normalization path:
def simple_buy_agent(
tickers, start_date, end_date, portfolio, model_name, model_provider, selected_analysts
):
# Return a dict matching the expected schema exactly
decisions = {t: {"action": "buy", "quantity": 10} for t in tickers}
return {"decisions": decisions}
Test it directly through the controller:
from src.backtesting.controller import AgentController
controller = AgentController()
output = controller.run_agent(
simple_buy_agent,
tickers=["AAPL", "MSFT"],
start_date="2023-01-01",
end_date="2023-01-31",
portfolio=portfolio, # a Portfolio instance
model_name="gpt-4o",
model_provider="openai",
selected_analysts=None,
)
print(output) # → {"decisions": {"AAPL": {"action": "buy", "quantity": 10}, ...}}
Custom Progress Handler for Debugging
Register a temporary handler to surface all status updates during execution:
from src.utils.progress import progress
def debug_handler(agent, ticker, status, analysis, timestamp):
print(f"[{timestamp}] {agent}:{ticker or ''} → {status}")
# Register the handler (remains active until unregistered)
progress.register_handler(debug_handler)
# Run the back-test; all updates will print to stdout
engine = BacktestEngine(...)
engine.run_backtest()
JavaScript SSE Client Implementation
Consume the stream in the browser to verify client-side behavior:
const evtSource = new EventSource("/hedge-fund/run");
evtSource.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log("Event:", data);
};
evtSource.onerror = (e) => {
console.error("Stream error", e);
evtSource.close();
};
If you receive only the initial StartEvent, check that the Python side calls progress.update_status at each agent milestone.
Summary
- Agent validation: Verify agents return dictionaries with lowercase action strings (
"buy","sell","hold") and numeric quantities; the controller insrc/backtesting/controller.pysilently neutralizes invalid outputs to"hold"/0 - Progress pipeline: Ensure
progress.register_handlerexecutes before the back-test begins, and confirmAgentProgress.update_statusbroadcasts updates to the asyncio queue insrc/utils/progress.py - Stream lifecycle: Inspect the async generator in
app/backend/routes/hedge_fund.pyfor proper task cancellation, client-disconnect detection viarequest.receive(), and the guaranteed emission ofCompleteEventbefore return - Trade execution: Remember that
execute_tradeinsrc/backtesting/trader.pyignores quantities ≤ 0 and requires validActionenum conversion
Frequently Asked Questions
Why is my agent returning decisions but trades aren't executing?
Check the normalization logic in src/backtesting/controller.py lines 44-58. If your agent returns "Buy" instead of "buy", the Action enum conversion fails and defaults to "hold". Additionally, verify in src/backtesting/trader.py line 18 that your quantity is a positive number—quantities ≤ 0 are filtered out before execution.
Why does the SSE stream stop after the StartEvent?
This indicates the asyncio queue is not receiving data. Verify that progress.update_status is being called for each agent iteration in src/utils/progress.py line 61. If running custom agents, ensure they emit progress events or manually call the update method. Also confirm the handler registration at line 75 of app/backend/routes/hedge_fund.py executes without error.
How do I detect when a client disconnects from the stream?
The wait_for_disconnect coroutine in app/backend/routes/hedge_fund.py (lines 53-58) polls request.receive() for the "http.disconnect" message. When detected, it triggers task cancellation. Log inside this block to track disconnections, and ensure your finally block (lines 42-53) properly cleans up handlers without raising secondary exceptions.
What happens if an agent returns an invalid action string?
According to src/backtesting/controller.py, the controller attempts to coerce the action to the Action enum. If coercion fails (e.g., "purchase" or "BUY"), the decision silently falls back to "hold" with zero quantity. Always use exact lowercase strings matching the enum values defined in the codebase.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →