跪拜 Guibai
← All articles
Interview

How Agent Frameworks Use JSON-RPC 2.0 and CompletableFuture to Prevent Tool-Call Deadlocks

By 神奇小汤圆 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

A hung tool call can freeze an entire Agent workflow if the main thread blocks on I/O. The CompletableFuture-plus-scheduler pattern shown here is a direct, copyable fix that keeps the control plane alive even when a subprocess misbehaves, and the protocol/transport split means the same timeout logic works identically for local scripts and remote HTTP services.

Summary

An LLM outputs a raw intent structure when it decides to call a tool, but a local Node.js script or a remote Python service cannot consume that object directly. Industrial Agent frameworks insert a protocol layer that translates the LLM's intent into a standard JSON-RPC 2.0 request, then route it through a transport layer—Stdio for local processes or HTTP for remote microservices. This orthogonal design keeps what gets sent separate from how it gets sent, so adding a new tool type never forces a rewrite of the upper logic.

The hardest engineering problem is a local script that enters an infinite loop and never returns a result. Instead of blocking the main thread on I/O, the protocol layer registers every outgoing request in a ConcurrentHashMap paired with a CompletableFuture. A scheduled timer fires after a configurable timeout and calls completeExceptionally, which unblocks the waiting business thread immediately. When a normal response arrives, the I/O thread finds the matching future and completes it; the ConcurrentHashMap.remove call ensures only one path—timeout or response—ever resolves the future.

A critical boundary exists: the timeout frees the Java thread but does not kill the runaway child process. Process-level cleanup is deferred to the transport's close method, which runs at server shutdown or restart. This layered approach keeps a single hung tool from cascading into a full system failure.

Takeaways
An LLM's tool-call output is just an intent object; a JSON-RPC 2.0 assembly layer translates it into a standard request that any tool service can consume.
Protocol layer (JsonRpcClient) handles request assembly, ID assignment, and pending-request lifecycle; transport layer (StdioTransport or HttpTransport) handles the actual I/O.
Routing is configuration-driven: a command field triggers a local subprocess via ProcessBuilder; a url field triggers an HTTP POST via OkHttp.
Every outgoing request gets a CompletableFuture stored in a ConcurrentHashMap, paired with a scheduled timeout task that calls completeExceptionally if no response arrives in time.
Using ConcurrentHashMap.remove to race the timeout against the normal response prevents duplicate completion of the same future.
A request-level timeout unblocks the calling thread but deliberately leaves the stuck child process alive; process destruction is deferred to transport.close at server shutdown.
Conclusions

The design treats a single hung tool call as a recoverable event for the control plane, not a fatal error—this is the difference between a demo Agent and one that can run unattended.

Deferring process kill to transport shutdown is a deliberate engineering trade-off: it avoids a premature kill -9 that could leave side effects or orphaned resources, at the cost of a zombie process lingering until the session ends.

The article frames the CompletableFuture pattern explicitly as interview rote material, which signals that Chinese backend teams now treat Agent infrastructure as a standard distributed-systems problem rather than an AI research topic.

Concepts & terms
JSON-RPC 2.0
A lightweight, transport-agnostic remote procedure call protocol that encodes requests and responses as JSON objects with a mandatory jsonrpc version field, a method name, params, and an id for matching responses to requests.
CompletableFuture
A Java concurrency class representing a future result that can be explicitly completed. It allows one thread to return a future immediately while another thread—or a scheduled timeout—completes it later, avoiding blocking the caller on I/O.
Orthogonal Design (Protocol vs. Transport)
Separating what gets sent (message format, ID management, timeouts) from how it gets sent (stdio pipes, HTTP connections) so that either layer can change independently without affecting the other.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗