In Volume 1, we examined why single-stream LLM outputs break at the TCP byte layer when UTF-8 characters and JSON objects split across network packet boundaries. But in modern multi-turn agent workflows, the network abstraction leaks at a far more dangerous layer: session state transport during asynchronous tool execution.
When an autonomous agent invokes a chain of external tools (querying a database, executing code in a sandbox, or calling third-party APIs), the generation stream pauses. To your frontend, the connection appears idle. To intermediate proxies, API gateways, and load balancers, an idle HTTP connection is dead weight marked for termination.
The architectural gap: Tool latency vs. proxy timeouts
In standard request-response lifecycles, latency is bounded. In multi-turn agent execution, model generation alternates between fast token streaming and long, silent tool execution phases:
The silent proxy black hole
Cloud load balancers, CDN edges, and enterprise corporate proxies enforce aggressive idle connection timeouts (often 30 to 60 seconds). When an agent pauses text generation to wait on an external tool (such as an asynchronous BigQuery query or a multi-step web retrieval), zero bytes flow across the wire.
The proxy silently drops the socket without sending a TCP FIN or RST packet to the client. The client UI sits permanently in a loading state, while the backend continues to execute and bill for unmonitored compute.
: ping\n\n) at sub-15-second intervals during asynchronous tool execution to maintain active TCP socket state (IETF RFC 9293) through intermediate proxies.The stateless reconnection trap
When a mobile client experiences a network handoff (e.g., switching from Wi-Fi to cellular) or recovers from a silent timeout, it initiates a reconnection. In naive serverless architectures:
- Ephemeral Instance Routing: The reconnected request lands on a different container instance (e.g., in Google Cloud Run) that lacks the in-memory stream buffer of the previous session.
- Duplicate Tool Execution: If the client blindly re-sends the original prompt, the agent re-executes non-idempotent tool calls (such as charging a payment or creating duplicate database rows).
- Token Buffer Thrashing: If the server replays the entire conversation history from scratch over the new stream, the client UI stutters, re-renders hundreds of tokens, and corrupts local scroll state.
The old way vs. the new way
Building production-grade multi-turn agent systems requires shifting from in-memory stream assumptions to durable, event-sourced session transport:
| Failure Mode | The Old Way (Naive In-Memory Streaming) | The New Way (Transactional Session Gateway) |
|---|---|---|
| Idle Tool Latency | Zero bytes sent during tool execution; proxy drops socket after 30s. | Background heartbeat emitter sends periodic SSE comments (: ping\n\n) every 10s. |
| Mid-Stream Disconnect | Stream state lost on container recycle; client restart aborts session. | Event-sourced log in Cloud Firestore records each token chunk and tool payload with a monotonic seq_id. |
| Reconnection Ingress | Client re-submits prompt, risking duplicate non-idempotent tool actions. | Client sends Last-Event-ID header; gateway replays only unacknowledged events from Firestore. |
| Tool Concurrency | Concurrent client retries trigger race conditions in parallel containers. | Distributed lease / mutex lock in Firestore ensures only one container executes tools per session. |
Tracks monotonic event sequence IDs, automatically reconnects with exponential backoff on transport drop, and passes Last-Event-ID for gap-free resumption.
Emits sub-15s keep-alive ping frames during tool execution, acquires atomic session locks, and streams model tokens while persisting event batches.
Maintains the authoritative append-only log of token chunks, tool call requests, and verified tool execution results for deterministic resumption.
Kill the connection mid-generation and watch idempotent reassembly recover exactly where TCP died.
Production-ready TypeScript implementation
Below is the production-tested AgentSessionStreamGateway implementation. It runs on Cloud Run or Firebase App Hosting, managing heartbeats during tool calls and enabling seamless reconnection via Last-Event-ID:
import { Response } from 'express';
import { Firestore } from '@google-cloud/firestore';
export interface StreamEvent {
seq: number;
type: 'token' | 'tool_start' | 'tool_end' | 'done' | 'error';
payload: unknown;
timestamp: number;
}
export class AgentSessionStreamGateway {
private heartbeatTimer?: NodeJS.Timeout;
private currentSeq: number = 0;
private eventBuffer: StreamEvent[] = [];
private readonly MAX_REPLAY_LIMIT = 200;
constructor(
private readonly sessionId: string,
private readonly res: Response,
private readonly db: Firestore
) {}
/**
* Initializes SSE response headers and begins periodic keep-alive pings.
*/
public initHeaders(): void {
this.res.setHeader('Content-Type', 'text/event-stream');
this.res.setHeader('Cache-Control', 'no-cache, no-transform');
this.res.setHeader('Connection', 'keep-alive');
this.res.setHeader('X-Accel-Buffering', 'no');
this.res.flushHeaders();
// Emit an SSE comment ping every 10 seconds to prevent proxy timeouts
this.heartbeatTimer = setInterval(() => {
if (!this.res.writableEnded) {
this.res.write(': ping\n\n');
}
}, 10_000);
}
/**
* Replays unacknowledged events with bounded limit on client reconnect.
*/
public async replayFrom(lastEventId: number): Promise<number> {
const snapshot = await this.db
.collection('agent_sessions')
.doc(this.sessionId)
.collection('events')
.where('seq', '>', lastEventId)
.orderBy('seq', 'asc')
.limit(this.MAX_REPLAY_LIMIT)
.get();
for (const doc of snapshot.docs) {
const event = doc.data() as StreamEvent;
this.writeSseFrame(event);
this.currentSeq = Math.max(this.currentSeq, event.seq);
}
return this.currentSeq;
}
/**
* Emits tokens instantly to client socket and buffers state for batched persistence.
*/
public emit(type: StreamEvent['type'], payload: unknown): void {
this.currentSeq += 1;
const event: StreamEvent = {
seq: this.currentSeq,
type,
payload,
timestamp: Date.now(),
};
// 1. Flush immediately to client socket (zero latency penalty on streaming)
this.writeSseFrame(event);
// 2. Buffer in memory for batched commit
this.eventBuffer.push(event);
// 3. Flush checkpoints immediately on tool execution boundaries
if (type !== 'token') {
void this.flushBuffer();
}
}
/**
* Flushes in-flight event buffer to Firestore using atomic batched writes.
*/
public async flushBuffer(): Promise<void> {
if (this.eventBuffer.length === 0) return;
const eventsToCommit = [...this.eventBuffer];
this.eventBuffer = [];
const batch = this.db.batch();
const sessionRef = this.db.collection('agent_sessions').doc(this.sessionId);
for (const event of eventsToCommit) {
const docId = event.seq.toString().padStart(8, '0');
const docRef = sessionRef.collection('events').doc(docId);
batch.set(docRef, event);
}
await batch.commit();
}
private writeSseFrame(event: StreamEvent): void {
if (this.res.writableEnded) return;
this.res.write(`id: ${event.seq}\n`);
this.res.write(`event: ${event.type}\n`);
this.res.write(`data: ${JSON.stringify(event.payload)}\n\n`);
}
public async close(): Promise<void> {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
await this.flushBuffer();
if (!this.res.writableEnded) {
this.res.end();
}
}
}Last-Event-ID reconnection to make your agent runtimes resilient against real-world network drops.Primary research and documentation
- WHATWG HTML Standard: Server-Sent Events (SSE) Protocol and Last-Event-ID
- Google Cloud Run: HTTPS Ingress, Timeouts, and Streaming Configuration
- Cloud Firestore: Atomic Transactions and Batched Operations
- Firebase AI Logic Documentation and Tool Calling Architecture
- Firebase App Check: Production Attestation for Streaming Backends
- IETF RFC 9293: Transmission Control Protocol (TCP) Specification
