← All posts

How to Stream What Your AI Agent Searches and Reads

Quick answer: Emit an event every time your agent runs a search or fetches a page, and push those events to the client over Server-Sent Events or a websocket. The client renders each search query and each fetched source the moment it happens, so users watch the agent work instead of staring at a spinner. It takes one event bus in your agent loop and a streaming endpoint on top of it.

A spinner that says "thinking" for twenty seconds erodes trust. A live feed that says "searching for X, reading page Y, found Z" builds it. When an agent uses the web, showing its work is not a nice-to-have. It is how users learn to believe the answer and how you debug the thing when it goes sideways.

Why Streaming Agent Activity Matters

Three reasons, in order of how often they bite.

Trust. Users trust an answer more when they can see where it came from. Watching the agent fetch three reputable sources feels different from a black box that returns a paragraph. The activity feed is the citation trail happening live.

Debugging. When an agent returns a bad answer, the first question is always "what did it read?" If you already stream every search and fetch, you have the trace for free. You can see the moment it searched for the wrong thing or fetched a low-quality page.

UX under latency. Agent runs are slow because they make real network calls. Streaming turns dead waiting time into visible progress. The perceived speed improves even when the total time does not.

What to Stream

You do not stream everything. You stream the events a human would want to narrate:

Event Payload Rendered as
search_started query "Searching: current EV tax credits"
search_results list of titles + URLs a collapsed list of candidates
fetch_started url, title "Reading: example.gov/ev-credit"
fetch_done url, word count a checkmark and a source chip
token partial answer text the answer streaming in
done final answer + citations the finished response

Keep payloads small. The client does not need the full fetched page, just enough to show what happened and link to the source.

The Core Pattern: An Event Bus in the Agent Loop

The cleanest design puts a single emit call at each interesting point in the agent loop. The loop does not know or care how events reach the user. That is the streaming layer's job.

def run_agent(query, emit):
    emit("search_started", {"query": query})
    results = search(query)                       # link.sc /v1/search
    emit("search_results", {"items": [
        {"title": r["title"], "url": r["url"]} for r in results
    ]})

    pages = []
    for r in results[:3]:
        emit("fetch_started", {"url": r["url"], "title": r["title"]})
        page = fetch_markdown(r["url"])           # link.sc /v1/fetch
        emit("fetch_done", {"url": r["url"], "words": len(page["markdown"].split())})
        pages.append(page)

    for chunk in stream_llm(query, pages):
        emit("token", {"text": chunk})
    emit("done", {"citations": [p["url"] for p in pages]})

Every emit becomes a message on the wire. The agent logic stays readable because the streaming concern is a single injected function.

Transport: SSE vs. Websockets

For streaming agent activity to a browser, the choice is almost always Server-Sent Events. The traffic is one-directional (server to client), SSE is plain HTTP so it passes through proxies cleanly, and browsers reconnect automatically.

SSE Websocket
Direction Server to client only Full duplex
Protocol Plain HTTP Upgrade handshake
Auto-reconnect Built in Roll your own
Best for Activity feeds, token streaming Chat with client-to-server mid-stream

Use a websocket only if the user needs to send messages while the agent is mid-run (interrupting, steering). For a read-only activity feed, SSE is less code and fewer edge cases.

The Streaming Endpoint

Here is an SSE endpoint that runs the agent and forwards each event as it fires. This example uses a queue so the agent (running in a worker thread) and the HTTP response (draining the queue) stay decoupled.

import json, queue, threading
from flask import Flask, request, Response

app = Flask(__name__)

@app.route("/agent/stream")
def stream():
    q = queue.Queue()

    def emit(event, data):
        q.put((event, data))

    def worker():
        try:
            run_agent(request.args["query"], emit)
        finally:
            q.put(("end", None))

    threading.Thread(target=worker, daemon=True).start()

    def events():
        while True:
            event, data = q.get()
            if event == "end":
                break
            yield f"event: {event}\ndata: {json.dumps(data)}\n\n"

    return Response(events(), mimetype="text/event-stream")

On the client, EventSource handles the rest:

const es = new EventSource(`/agent/stream?query=${encodeURIComponent(q)}`);

es.addEventListener("search_started", (e) =>
  addRow("Searching: " + JSON.parse(e.data).query));

es.addEventListener("fetch_started", (e) =>
  addSource(JSON.parse(e.data)));

es.addEventListener("token", (e) =>
  appendAnswer(JSON.parse(e.data).text));

es.addEventListener("done", (e) => {
  showCitations(JSON.parse(e.data).citations);
  es.close();
});

Each event type maps to a small UI update. The feed builds itself as the agent works.

The Product Angle: Observability of Web Use

Streaming is not only a UX trick. It is the foundation of observability for an agent's web use. The same event stream you show users can be logged, so you get:

  • A per-run trace of every query and every URL, useful for support and audits.
  • Metrics on how many sources a typical answer draws from.
  • A fast way to spot when the agent is fetching junk or looping on the same query.

Because link.sc returns clean markdown and structured search results, the events carry meaningful titles and word counts rather than opaque blobs, which makes the trace readable by a human without extra parsing.

Keep the Feed Honest

A live feed is a promise that you are showing the real work. Keep it honest: emit the actual URLs fetched, not a curated subset, and show fetch failures too. If the agent tried a source and it did not load, say so. A feed that hides the messy parts is worse than no feed, because it builds trust the answer has not earned.

Where This Fits

Streaming pairs naturally with the search-then-fetch loop. If you have not built that yet, combine search and fetch in one pipeline is the retrieval half, and this post is the visibility half on top of it. For a full agent that runs the loop itself, see build a deep research agent. Endpoint parameters are in the link.sc docs.

Put an emit at every search and fetch, push it over SSE, and your agent stops being a black box. Users see what it reads, you see what it did, and both of you trust the result more.


Build an agent whose web work is visible in real time. Start free at link.sc.