<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nozokime</title>
    <description>The latest articles on DEV Community by Nozokime (@ckx3197273653).</description>
    <link>https://dev.to/ckx3197273653</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4175616%2F34cec9c6-c91f-41c8-8ed0-996ff24e3df5.jpg</url>
      <title>DEV Community: Nozokime</title>
      <link>https://dev.to/ckx3197273653</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ckx3197273653"/>
    <language>en</language>
    <item>
      <title>Why ThreadLocal Fails Silently in Streaming Agents: Zero-Intrusion Tool-Call Tracing in LangChain4j</title>
      <dc:creator>Nozokime</dc:creator>
      <pubDate>Sat, 10 Oct 2026 16:43:49 +0000</pubDate>
      <link>https://dev.to/ckx3197273653/why-threadlocal-fails-silently-in-streaming-agents-zero-intrusion-tool-call-tracing-in-langchain4j-57l3</link>
      <guid>https://dev.to/ckx3197273653/why-threadlocal-fails-silently-in-streaming-agents-zero-intrusion-tool-call-tracing-in-langchain4j-57l3</guid>
      <description>&lt;p&gt;While adding real-time tool-call tracing to a QC analysis agent, we went down two dead ends before landing on a zero-intrusion design: one agent instance per request, plus closure capture. This post documents the full journey — and why both seemingly perfect solutions fail silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Problem: Let the Frontend "See" What the Agent Is Doing
&lt;/h2&gt;

&lt;p&gt;The setting: a quality-inspection analysis agent built with Spring Boot 4 and LangChain4j 1.14.1. A user asks "analyze the root cause of report R20260808001", and the model autonomously decides which tools to call — &lt;code&gt;getReportByNo&lt;/code&gt; → &lt;code&gt;getReport&lt;/code&gt; → &lt;code&gt;getDefects&lt;/code&gt; → &lt;code&gt;getProductionParams&lt;/code&gt; → &lt;code&gt;searchKnowledge&lt;/code&gt; — before writing its conclusion.&lt;/p&gt;

&lt;p&gt;The traditional UX: the user stares at a spinner, then receives a wall of text seconds later. &lt;strong&gt;The process is a black box.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our approach: push the tool-call trace to the frontend over SSE, rendered as running/done cards, so users can watch the agent "work":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fuser-attachments%2Fassets%2F9cd1548f-904a-47dd-908c-99d57a3b79bf" class="article-body-image-wrapper"&gt;&lt;img width="760" height="428" alt="工具滚动" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fuser-attachments%2Fassets%2F9cd1548f-904a-47dd-908c-99d57a3b79bf"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When a tool starts executing → push a &lt;code&gt;running&lt;/code&gt; event (with arguments)&lt;/li&gt;
&lt;li&gt;When a tool finishes → push a &lt;code&gt;done&lt;/code&gt; event (with the result)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No instrumentation code inside the tools themselves&lt;/strong&gt; (zero intrusion)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first two are easy. The third is hard — plus a blocker we didn't see coming: &lt;strong&gt;the SSE &lt;code&gt;emitter&lt;/code&gt; is a per-request object, but tool execution happens deep inside the model call, across thread boundaries. How do you get the emitter into the callback?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. First, Look at Where the Threads Are
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjgkuv19ztsf6233cauc2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjgkuv19ztsf6233cauc2.png" alt=" " width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To sum it up in one sentence: &lt;strong&gt;the emitter is born on the servlet thread, but has to be used on the executor thread — and potentially a tool thread-pool thread.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Dead End #1: ThreadLocal — Looks Perfect, Fails Silently
&lt;/h2&gt;

&lt;p&gt;Your first instinct is almost certainly ThreadLocal: &lt;code&gt;set&lt;/code&gt; it when the request arrives, &lt;code&gt;get&lt;/code&gt; it in the callback. So elegant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;TraceContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emitter&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;analyze&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// inside the callback:&lt;/span&gt;
&lt;span class="nc"&gt;AgentTraceContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TraceContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"tool"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's tempting: tool code stays clean, and callbacks just call &lt;code&gt;get()&lt;/code&gt;. But it dies — in two distinct ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake A: dies today.&lt;/strong&gt; Set it in the controller method (servlet thread), read it in a callback running on the &lt;code&gt;streamExecutor&lt;/code&gt; thread → you get &lt;code&gt;null&lt;/code&gt;. ThreadLocal is &lt;em&gt;thread-local&lt;/em&gt; storage; it doesn't cross threads by design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake B: works today, dies silently tomorrow.&lt;/strong&gt; Set it inside the executor lambda instead. In the current code, tools run sequentially and the model call is synchronous, so everything happens to run on that same executor thread — and it works. But that rests on two fragile assumptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assuming tools &lt;strong&gt;always run sequentially&lt;/strong&gt; — enable &lt;code&gt;executeToolsConcurrently()&lt;/code&gt; and tools get dispatched to a separate thread pool; the callback runs on a pool thread → &lt;code&gt;get()&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Assuming the model is &lt;strong&gt;always synchronous&lt;/strong&gt; — switch to &lt;code&gt;StreamingChatModel&lt;/code&gt; and callbacks run on async callback threads → same problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal repro (20 lines, runnable):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocalDemo&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;CTX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadLocal&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ExecutorService&lt;/span&gt; &lt;span class="no"&gt;POOL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Executors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newSingleThreadExecutor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="no"&gt;CTX&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"the emitter for this request"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="no"&gt;POOL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"read: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="no"&gt;CTX&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;// expected: emitter&lt;/span&gt;
        &lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;                                            &lt;span class="c1"&gt;// actual: null&lt;/span&gt;
        &lt;span class="no"&gt;POOL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;shutdown&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scariest part isn't the failure — it's &lt;em&gt;how&lt;/em&gt; it fails: &lt;strong&gt;silently&lt;/strong&gt;. No exception, no error log; the frontend just never receives events. You can set a breakpoint in the controller and everything looks fine, yet in the callback &lt;code&gt;get()&lt;/code&gt; is always &lt;code&gt;null&lt;/code&gt; — you spend an afternoon on it and start questioning your sanity.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Dead End #2: &lt;code&gt;InvocationContext.methodArguments()&lt;/code&gt; — Framework Internals
&lt;/h2&gt;

&lt;p&gt;While digging, you'll come across &lt;code&gt;InvocationContext&lt;/code&gt; in LangChain4j's callback context. It looks like you can read method arguments from it, so the temptation is to smuggle the emitter through the argument list.&lt;/p&gt;

&lt;p&gt;Don't. That mechanism is how the framework internally locates managed types like &lt;code&gt;ChatMemory&lt;/code&gt; (&lt;code&gt;managedParameters&lt;/code&gt; / &lt;code&gt;LangChain4jManaged&lt;/code&gt;). Using it as a custom argument channel is undocumented behavior — it can break on any framework upgrade.&lt;/p&gt;

&lt;p&gt;Lesson: &lt;strong&gt;just because a framework leaves a door open doesn't mean it's meant for you to walk through.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The Solution: One Agent Instance Per Request + Closure Capture
&lt;/h2&gt;

&lt;p&gt;The core idea in one sentence: &lt;strong&gt;the context shouldn't live on a thread or inside framework internals — it should be instance state.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 The Three Components
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;AgentFactory&lt;/strong&gt; — builds one agent instance per request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@FunctionalInterface&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;AgentFactory&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;QcAnalysisAgent&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SseEmitter&lt;/span&gt; &lt;span class="n"&gt;emitter&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nd"&gt;@Profile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cloud"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;AgentFactory&lt;/span&gt; &lt;span class="nf"&gt;cloudAgentFactory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QcAgentTools&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Build the model once: connection pool, API key and the token-usage&lt;/span&gt;
    &lt;span class="c1"&gt;// listener all live on the model&lt;/span&gt;
    &lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAiChatModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="o"&gt;(...)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;(...)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;modelName&lt;/span&gt;&lt;span class="o"&gt;(...)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listeners&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TokenUsageListener&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Build the agent once per request: the closure binds the emitter to it&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;emitter&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;buildAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emitter&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;QcAnalysisAgent&lt;/span&gt; &lt;span class="nf"&gt;buildAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatModel&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;QcAgentTools&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                   &lt;span class="nc"&gt;SseEmitter&lt;/span&gt; &lt;span class="n"&gt;emitter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AiServices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;QcAnalysisAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatModel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;beforeToolExecution&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// official hook 1: execution starts&lt;/span&gt;
                &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LinkedHashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
                &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
                &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"args"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
                &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"running"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="nc"&gt;AgentTraceContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emitter&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"tool"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;})&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AgentTraceListener&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emitter&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;// official hook 2: execution done&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AgentTraceListener&lt;/strong&gt; — the tool-completion hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentTraceListener&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ToolExecutedEventListener&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;SseEmitter&lt;/span&gt; &lt;span class="n"&gt;emitter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// captured by the closure, travels with the instance&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolExecutedEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LinkedHashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"done"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"result"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;truncate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resultText&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
        &lt;span class="nc"&gt;AgentTraceContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emitter&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"tool"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why the closure works&lt;/strong&gt;: once the emitter is captured by the lambda, it travels with the agent instance — it doesn't depend on "which thread am I on right now". Whichever thread fires the callback, the emitter inside the closure is always the one for this request. ThreadLocal's "thread-bound" assumption is bypassed entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 The Cost Tradeoff: Build the Expensive Thing Once, the Cheap Thing Per Request
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model (expensive)&lt;/strong&gt;: connection pool, API key, token-usage listener — built once, bean-scoped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent (cheap)&lt;/strong&gt;: measured &lt;code&gt;AiServices.builder().build()&lt;/code&gt; at a steady-state average of &lt;strong&gt;0.27ms&lt;/strong&gt; (500 warm-up iterations, then 5000 measured, JDK 17). A full analysis takes about 5.2s (from logs), so the build overhead is roughly &lt;strong&gt;0.005%&lt;/strong&gt; — negligible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One honest small number: the very first build in a fresh JVM took 352ms (internal class loading + JIT compilation). It's a one-time cost that only affects the first request. Warm it up at startup if you care; ignore it if you don't.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 Proof of Non-Intrusion
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;QcAgentTools&lt;/code&gt; contains four tools, and the entire file is business logic plus plain &lt;code&gt;log.info&lt;/code&gt; lines — &lt;strong&gt;not a single line of instrumentation&lt;/strong&gt;. Compare this with the intrusive alternative: every new tool would need two hand-written push calls, every tool author would have to know about "tracing", and forgetting one line means silently losing events. With this design, adding a new tool means caring about business logic only — the tool code knows nothing about tracing.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Summary
&lt;/h2&gt;

&lt;p&gt;ThreadLocal fails silently in a streaming agent because of &lt;strong&gt;thread hops&lt;/strong&gt;: the emitter is created on the servlet thread but consumed on executor / tool-pool / async-callback threads. Both dead ends (ThreadLocal, &lt;code&gt;InvocationContext.methodArguments()&lt;/code&gt;) try to carry request context across threads — one via thread binding, the other via framework internals. Neither is reliable.&lt;/p&gt;

&lt;p&gt;The final design moves the context from "thread" to "instance": &lt;strong&gt;one agent instance per request, with the emitter bound to the instance through closure capture&lt;/strong&gt;. Whichever thread fires the callback, the emitter in the closure is always the right one. The model is built once (expensive), the agent once per request (0.27ms, cheap) — zero intrusion, zero silent failures.&lt;/p&gt;

&lt;p&gt;You can call this pattern &lt;strong&gt;Per-request Instance + Closure-captured Context&lt;/strong&gt;. It applies beyond SSE tracing: audit logs, per-request rate-limit markers, traceId propagation — the same family of problems. A few production details worth copying: silently skip pushes when the client disconnects, use &lt;code&gt;LinkedHashMap&lt;/code&gt; instead of &lt;code&gt;Map.of&lt;/code&gt; (values can be null), and truncate results to 800 characters.&lt;/p&gt;

&lt;p&gt;Full implementation: &lt;a href="https://github.com/CKX3197273653/Nozomi" rel="noopener noreferrer"&gt;github.com/CKX3197273653/Nozomi&lt;/a&gt; (package &lt;code&gt;mate10/src/main/java/org/mate/mate10/agent/&lt;/code&gt;, written against LangChain4j 1.14.1)&lt;/p&gt;

</description>
      <category>java</category>
      <category>langchain4j</category>
    </item>
  </channel>
</rss>
