<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged langgraph</title>
<link>https://coderlegion.com/tag/langgraph</link>
<description>Powered by Question2Answer</description>
<item>
<title>Stop calling functions: my first LangGraph graph, where state does the calling</title>
<link>https://coderlegion.com/32792/stop-calling-functions-my-first-langgraph-graph-where-state-does-the-calling</link>
<description>&lt;p&gt;Three parts in, JobCopilot had layers that worked but couldn't talk to each other. Part 1 gave me a structured-output layer — &lt;code&gt;ParseOk&lt;/code&gt;/&lt;code&gt;ParseError&lt;/code&gt; as data. Part 2 gave me retrieval — &lt;code&gt;build_context()&lt;/code&gt; pulling the top-8 resume chunks for a job description. The pipeline was one line on paper; in code it was nested calls, and the awkward questions had no home: who retries when the model drifts into prose? Who &lt;em&gt;stops&lt;/em&gt; the retries? Where does the score live between &quot;parsed&quot; and &quot;shown to the user&quot;?&lt;/p&gt;
&lt;p&gt;I did what a React dev does in a new language: I nested functions. The retry loop closed over a closure that closed over a callback, and I couldn't tell you who owned what. Rewriting the flow as a graph — my first with &lt;a rel=&quot;nofollow&quot; href=&quot;https://python.langchain.com/docs/introduction/&quot;&gt;LangGraph&lt;/a&gt; — made the nesting disappear. Not because a framework absorbed it, but because the mental model changed: &lt;strong&gt;functions stop calling each other; state flows through a graph, and nodes return patches of that state.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It survived everything I threw at it — after I fixed one wire that made &lt;em&gt;every&lt;/em&gt; job come back &lt;code&gt;&quot;error&quot;&lt;/code&gt;, no matter how perfectly the model answered.&lt;/p&gt;
&amp;lt;!-- NOTE TO V: the JobCopilot mirror isn't on this machine, so code below is the canonical pattern rebuilt from the part-1/part-2 contracts (ParseOk/ParseError, build_context, TOP_K=8). Before publishing: align file names, node names and prompt wording with your repo. --&amp;gt;
&lt;h3 id=&quot;1-the-state-is-a-pydantic-model-nodes-return-patches&quot;&gt;1. The state is a pydantic model; nodes return patches&lt;/h3&gt;
&lt;p&gt;If the graph is the wiring, state is everything a run knows. I made it one pydantic model — deliberately the &lt;em&gt;same&lt;/em&gt; discipline as part 1:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# state.py — the whole run in one model
from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field

THRESHOLD = 60   # score at or above this = &quot;worth applying&quot;
MAX_CALLS = 3    # 1 first try + 2 repairs — the budget from part 1


class AgentState(BaseModel):
    &quot;&quot;&quot;One run of the copilot, end to end.&quot;&quot;&quot;

    jd_text: str = &quot;&quot;
    context: str = &quot;&quot;              # resume chunks, from part 2
    last_raw: str = &quot;&quot;             # what the model last said, verbatim
    error: str | None = None       # latest parse failure, if any
    attempt: int = 0               # how many LLM calls so far

    match_score: int | None = None
    missing_skills: list[str] = Field(default_factory=list)
    verdict: Literal[&quot;match&quot;, &quot;no_match&quot;, &quot;error&quot;] | None = None
&lt;/code&gt;&lt;/pre&gt;
&amp;lt;!-- NOTE TO V: `THRESHOLD = 60` and `MAX_CALLS = 3` are illustrative constants; swap in your real repo values. --&amp;gt;
&lt;p&gt;The rule that makes a graph a graph: &lt;strong&gt;a node never calls another node.&lt;/strong&gt; It receives state, does one job, and returns a &lt;code&gt;dict&lt;/code&gt; with &lt;em&gt;only what changed&lt;/em&gt; — a patch. LangGraph merges the patch and hands state to the next node. &lt;code&gt;retrieve&lt;/code&gt; fills &lt;code&gt;context&lt;/code&gt;; &lt;code&gt;judge&lt;/code&gt; fills the score fields; nobody knows about anybody else.&lt;/p&gt;
&lt;p&gt;Fields without a custom reducer are &lt;strong&gt;last-writer-wins&lt;/strong&gt; — I proved it to myself before trusting it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def note_a(state: AgentState) -&amp;gt; dict:
    return {&quot;missing_skills&quot;: [&quot;Python&quot;]}

def note_b(state: AgentState) -&amp;gt; dict:
    return {&quot;missing_skills&quot;: [&quot;Terraform&quot;]}

# a -&amp;gt; b in that order: missing_skills == [&quot;Terraform&quot;] — b overwrote a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For my state that's correct by design: every field is overwrite-on-purpose, so zero reducers. If a list should &lt;em&gt;accumulate&lt;/em&gt; across nodes, opt in with &lt;code&gt;Annotated&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from typing import Annotated


def merge_lists(left: list[str] | None, right: list[str] | None) -&amp;gt; list[str]:
    if left is None:
        return right or []
    if right is None:
        return left
    return list(left) + [x for x in right if x not in left]


class AccumState(BaseModel):
    missing_skills: Annotated[list[str], merge_lists] = Field(default_factory=list)

# two nodes appending &quot;Python&quot; then &quot;Terraform&quot; -&amp;gt; [&quot;Python&quot;, &quot;Terraform&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And why pydantic for state instead of the &lt;code&gt;TypedDict&lt;/code&gt; every tutorial starts with? A patch is data from somewhere else, and at merge time nobody type-checks a plain dict:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-pycon&quot;&gt;# TypedDict state, merged by dict spread — type-checkers scream, Python shrugs:
&amp;gt;&amp;gt;&amp;gt; {**{&quot;attempt&quot;: 0, &quot;verdict&quot;: None}, &quot;verdict&quot;: 21, &quot;attempt&quot;: &quot;two&quot;}
{&quot;attempt&quot;: &quot;two&quot;, &quot;verdict&quot;: 21}          # garbage, happily stored

# pydantic model:
&amp;gt;&amp;gt;&amp;gt; PS(attempt=&quot;two&quot;, verdict=21)
pydantic.ValidationError: attempt — Input should be a valid integer...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same lesson as part 1: &lt;strong&gt;shape is a contract, and contracts live in pydantic, not in comments.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-the-graph-is-a-handful-of-declared-edges&quot;&gt;2. The graph is a handful of declared edges&lt;/h3&gt;
&lt;p&gt;Here is the entire &lt;code&gt;graph.py&lt;/code&gt; — wiring, not logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# graph.py
from langgraph.graph import END, START, StateGraph

from nodes import Chat, fail_node, judge_node, retrieve_node, route_after_parse
from state import AgentState


def build_graph(chat: Chat):
    g = StateGraph(AgentState)

    g.add_node(&quot;retrieve&quot;, retrieve_node)
    g.add_node(&quot;judge&quot;, lambda s: judge_node(s, chat))  # closure: node gets the LLM client
    g.add_node(&quot;fail&quot;, fail_node)

    g.add_edge(START, &quot;retrieve&quot;)
    g.add_edge(&quot;retrieve&quot;, &quot;judge&quot;)

    g.add_conditional_edges(
        &quot;judge&quot;,
        route_after_parse,          # returns &quot;ok&quot; | &quot;retry&quot; | &quot;give_up&quot;
        {&quot;ok&quot;: END, &quot;retry&quot;: &quot;judge&quot;, &quot;give_up&quot;: &quot;fail&quot;},
    )
    g.add_edge(&quot;fail&quot;, END)

    return g.compile()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Coming from React, this shape clicked instantly: it's a component tree. Components don't import each other to call each other — data flows down a declared structure, and the structure decides what happens next.&lt;/p&gt;
&amp;lt;!-- NOTE TO V: align node names (`retrieve` / `judge` / `fail`) and the `chat(prompt)` thin-client signature with your repo; `chat` is the same one-call LLM wrapper the structured-output article assumed. --&amp;gt;
&lt;p&gt;The &lt;code&gt;lambda&lt;/code&gt; is deliberate: the graph is built once with a concrete LLM client, and no node ever imports a provider. That's what makes the graph testable — hand it a fake &lt;code&gt;chat&lt;/code&gt; and the whole flow runs with zero network. Which is exactly how I caught the bug.&lt;/p&gt;
&lt;h3 id=&quot;3-all-routing-logic-in-one-boring-function-and-the-wire-that-said-error-to-everything&quot;&gt;3. All routing logic in one boring function — and the wire that said &quot;error&quot; to everything&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;add_conditional_edges&lt;/code&gt; takes a &lt;em&gt;router&lt;/em&gt;: a pure function from state to a string, plus a mapping from strings to destinations. Mine is twelve lines and holds the entire loop policy of the agent:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# nodes.py
from state import MAX_CALLS


def route_after_parse(state: AgentState) -&amp;gt; str:
    &quot;&quot;&quot;All loop logic in one boring function.&quot;&quot;&quot;
    if state.error is None:
        return &quot;ok&quot;
    if state.attempt &amp;lt; MAX_CALLS:
        return &quot;retry&quot;
    return &quot;give_up&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compare with my first wiring attempt, written &lt;em&gt;because&lt;/em&gt; the router concept felt foreign:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;g.add_edge(&quot;retrieve&quot;, &quot;judge&quot;)
g.add_edge(&quot;judge&quot;, &quot;fail&quot;)     # &quot;temporarily — I'll wire the real branches next&quot;
&lt;/code&gt;&lt;/pre&gt;
&amp;lt;!-- NOTE TO V: adjust this anecdote to what actually happened in your repo; keep the lesson either way. --&amp;gt;
&lt;p&gt;Then I ran it — with a fake &lt;code&gt;chat&lt;/code&gt; returning a &lt;em&gt;perfect&lt;/em&gt; JSON verdict. Result: &lt;code&gt;verdict = &quot;error&quot;&lt;/code&gt;. Every single job. The parse was flawless, the score valid, and the graph marched it into &lt;code&gt;fail_node&lt;/code&gt; anyway: I had declared exactly one outgoing edge from &lt;code&gt;judge&lt;/code&gt;, and a graph never &quot;falls through&quot;. Nothing raised, nothing warned — the state machine politely followed the only road I built:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;gt;&amp;gt;&amp;gt; # plain edge judge -&amp;gt; fail, no conditional edges, perfect JSON from the model
&amp;gt;&amp;gt;&amp;gt; app.invoke(AgentState(jd_text=&quot;junior&quot;))
verdict = &quot;error&quot;     # a valid parse, executed through the wrong edge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix was the conditional edges above, plus two habits that came with it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Route values are data, not flow control.&lt;/strong&gt; If the router returns a key that's not in the mapping, LangGraph doesn't guess — it throws &lt;code&gt;KeyError&lt;/code&gt; at runtime. I know because my typo did exactly that. An unmapped route crashes loudly; a wrongly-wired edge fails silently. I'll take the crash.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Loops are declared, not nested.&lt;/strong&gt; &lt;code&gt;&quot;retry&quot;: &quot;judge&quot;&lt;/code&gt; makes a cycle in the graph. The loop exists as an edge, its budget lives in &lt;code&gt;MAX_CALLS&lt;/code&gt;, and its stop condition is one pure function I can unit-test without LLM, network, or shame.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The shift, internalized: in nested functions, control flow was &lt;em&gt;who calls whom&lt;/em&gt; — invisible at a glance. In the graph, it's &lt;em&gt;data in the edges&lt;/em&gt; — visible in one screenful.&lt;/p&gt;
&lt;h3 id=&quot;4-closing-the-repair-loop-with-state-not-recursion&quot;&gt;4. Closing the repair loop with state, not recursion&lt;/h3&gt;
&lt;p&gt;Last piece: &lt;code&gt;judge_node&lt;/code&gt;, the only node that talks to the model. It reads state to decide &lt;em&gt;which&lt;/em&gt; prompt to send — first pass or repair — and returns a patch either way:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# nodes.py
from schemas import ParseError, ParseOk, parse_match
from state import AgentState, THRESHOLD

MATCH_PROMPT = &quot;&quot;&quot;You are a skeptical technical recruiter.
Compare the job description to the candidate's resume excerpt.
Reply with ONLY a JSON object:
{{&quot;match_score&quot;: &amp;lt;int 0-100&amp;gt;, &quot;missing_skills&quot;: [&amp;lt;string, ...&amp;gt;]}}

JOB DESCRIPTION:
{jd}

RESUME EXCERPT:
{context}
&quot;&quot;&quot;

FIX_PROMPT = &quot;&quot;&quot;Your previous reply could not be parsed.

PREVIOUS REPLY:
{previous}

PARSER ERROR:
{error}

Reply with ONLY a valid JSON object:
{{&quot;match_score&quot;: &amp;lt;int 0-100&amp;gt;, &quot;missing_skills&quot;: [&amp;lt;string, ...&amp;gt;]}}
&quot;&quot;&quot;


def judge_node(state: AgentState, chat: Chat) -&amp;gt; dict:
    &quot;&quot;&quot;One LLM call, one parse, a patch of state. Loops are the graph's job now.&quot;&quot;&quot;
    if state.error is None:
        prompt = MATCH_PROMPT.format(jd=state.jd_text, context=state.context)
    else:
        prompt = FIX_PROMPT.format(previous=state.last_raw, error=state.error)

    raw = chat(prompt)
    result = parse_match(raw)          # the part-1 contract, unchanged

    patch: dict = {&quot;last_raw&quot;: raw, &quot;attempt&quot;: state.attempt + 1}
    if isinstance(result, ParseOk):
        score = result.data.match_score
        patch.update(
            match_score=score,
            missing_skills=result.data.missing_skills,
            verdict=&quot;match&quot; if score &amp;gt;= THRESHOLD else &quot;no_match&quot;,
            error=None,
        )
    else:
        patch.update(error=result.message)
    return patch


def fail_node(state: AgentState) -&amp;gt; dict:
    &quot;&quot;&quot;The model never produced valid JSON. Say so — as state, not a stack trace.&quot;&quot;&quot;
    return {&quot;verdict&quot;: &quot;error&quot;}
&lt;/code&gt;&lt;/pre&gt;
&amp;lt;!-- NOTE TO V: these prompts are skeleton versions; align wording with your actual `MATCH_PROMPT`/`FIX_PROMPT` from the structured-output layer. --&amp;gt;
&lt;p&gt;Because &lt;code&gt;build_graph&lt;/code&gt; takes &lt;code&gt;chat&lt;/code&gt; as a parameter, the end-to-end test is a fake answer card — call one drifts into prose, call two complies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def flaky_llm():
    calls = {&quot;n&quot;: 0}

    def chat(prompt: str) -&amp;gt; str:
        calls[&quot;n&quot;] += 1
        if calls[&quot;n&quot;] == 1:
            return &quot;Sure! The match is quite good, I'd say around 70 out of 100.&quot;
        return '{&quot;match_score&quot;: 70, &quot;missing_skills&quot;: [&quot;Terraform&quot;]}'

    return chat


app = build_graph(flaky_llm())
final = app.invoke(AgentState(jd_text=&quot;Senior Frontend Engineer (React, TS)&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;verdict: match | score: 70 | attempts: 2 | error: None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The prose landed in &lt;code&gt;last_raw&lt;/code&gt;, &lt;code&gt;parse_match&lt;/code&gt; filled &lt;code&gt;error&lt;/code&gt;, the router said &lt;code&gt;&quot;retry&quot;&lt;/code&gt;, and the second call got &lt;code&gt;FIX_PROMPT&lt;/code&gt; with the model's own prose quoted back at it — the part-1 trick, now &lt;em&gt;driven by edges instead of a while-loop&lt;/em&gt;. When the model never complies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;verdict: error | error: no JSON object found in model output | attempts: 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No exception slicing through three stack frames. &lt;code&gt;fail_node&lt;/code&gt; writes &lt;code&gt;verdict: &quot;error&quot;&lt;/code&gt; and the graph ends — for an agent run overnight over forty postings, a run that &lt;em&gt;ends with a recorded failure&lt;/em&gt; beats ten runs that crashed and took the batch with them. Part 1's &quot;errors as data&quot;, promoted from one function to the shape of the whole system.&lt;/p&gt;
&amp;lt;!-- NOTE TO V: outputs above are from my fake-LLM smoke run (langgraph 1.2.15, pydantic 2.14.1); re-check against your repo's pinned versions. Worth re-confirming on your version: last-writer-wins merge and `KeyError` on an unmapped route. --&amp;gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;The first graph, in one sentence: &lt;strong&gt;state is a pydantic contract, nodes return patches, a router returns strings, loops live in edges.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Three things this React-dev-learning-Python took from it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Patches, not calls.&lt;/strong&gt; The merge is last-writer-wins by default; accumulators are opt-in via &lt;code&gt;Annotated&lt;/code&gt;. Every field in my state is overwrite-on-purpose — zero reducers, but verified behavior before trusting it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Routing is a pure function.&lt;/strong&gt; Twelve lines, no LLM, unit-testable — and an unmapped return value crashes with a &lt;code&gt;KeyError&lt;/code&gt;. That's the framework refusing to guess, and I've come to love it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Giving up is a node, not a throw.&lt;/strong&gt; &lt;code&gt;fail&lt;/code&gt; writes &lt;code&gt;verdict: &quot;error&quot;&lt;/code&gt; and the graph ends cleanly. The batch survives; the log tells the truth.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And the bug that taught me most: a graph executes exactly the edges you declared — my single &lt;code&gt;judge → fail&lt;/code&gt; wire turned perfect parses into errors with zero pushback from the framework. A state machine has no common sense. That's the feature.&lt;/p&gt;
&lt;p&gt;Next Tuesday, part 4: evals. The judge says 72 — but is 72 &lt;em&gt;right&lt;/em&gt;? Fixtures of job descriptions with expected score ranges, a negative case that must score LOW (a Python-backend posting should &lt;em&gt;not&lt;/em&gt; match a frontend resume), and the first time JobCopilot tells me I'm wrong.&lt;/p&gt;
&lt;p&gt;Follow if you want to watch the rest of the copilot get built — including the parts that break. Build log + daily code: &lt;a rel=&quot;nofollow&quot; href=&quot;https://t.me/novamind_hub&quot;&gt;t.me/novamind_hub&lt;/a&gt;. Part 1, in case you missed it: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/veaceslav/stop-prompting-for-valid-json-an-llm-output-layer-that-does-not-break-5264&quot;&gt;Stop prompting for valid JSON: an LLM output layer that does not break&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Do you build agent graphs with a framework or hand-roll the loop? And what's the first thing your graph did that you didn't expect?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32792/stop-calling-functions-my-first-langgraph-graph-where-state-does-the-calling</guid>
<pubDate>Wed, 28 Oct 2026 09:39:31 +0000</pubDate>
</item>
<item>
<title>LangGraph4j 1.9.0: What's new and the roadmap</title>
<link>https://coderlegion.com/27744/langgraph4j-1-9-0-whats-new-and-the-roadmap</link>
<description>&lt;h3 id=&quot;langgraph4j-1-9-0-is-out-&quot;&gt;LangGraph4j 1.9.0 is out!&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/langgraph4j/langgraph4j&quot;&gt;LangGraph4j&lt;/a&gt; 1.9.0 is out!&lt;/strong&gt; With the &lt;code&gt;1.8.x&lt;/code&gt; release stream now in LTS, development moves forward on &lt;code&gt;1.9&lt;/code&gt; with improvements to streaming, persistence, and the infrastructure for building agents in Java.&lt;/p&gt;
&lt;p&gt;This release is an important step toward workflows that are easier to follow while they run and easier to inspect after they finish. A node can now report progress output before returning its result, checkpoint savers have a richer execution lifecycle, and Studio gets an UI refresh.&lt;/p&gt;
&lt;p&gt;Let's look at what changes for an application built on &lt;code&gt;1.8.x&lt;/code&gt;, with particular attention to &lt;strong&gt;custom output from nodes&lt;/strong&gt; and &lt;strong&gt;checkpoint management&lt;/strong&gt;. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://langgraph4j.github.io/langgraph4j/1.9/whats-new-v1.9&quot;&gt;official migration guide&lt;/a&gt; provides details API reference for the changes discussed here.&lt;/p&gt;
&lt;h3 id=&quot;emit-custom-output-while-a-node-is-running&quot;&gt;Emit custom output while a node is running&lt;/h3&gt;
&lt;p&gt;Imagine a node that retrieves documents, processes them, and prepares a response. The caller may want to show progress throughout that work. Waiting for the final node output gives the user minimal information about what is happening in the meantime.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;1.9&lt;/code&gt;, a node action can emit its own typed &lt;code&gt;NodeOutput&lt;/code&gt; values through the active graph execution stream (&lt;code&gt;graph.stream(...)&lt;/code&gt;). They arrive alongside the usual START, node, and END outputs, before the node has necessarily finished.&lt;/p&gt;
&lt;p&gt;The mechanism is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define a subclass of &lt;code&gt;NodeOutput&lt;/code&gt; carrying the information you want to expose.&lt;/li&gt;
&lt;li&gt;Obtain a dispatcher from the node's &lt;code&gt;RunnableConfig&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Dispatch custom outputs during execution.&lt;/li&gt;
&lt;li&gt;Handle those types when consuming the graph stream.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here is a small example.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public final class ProgressOutput extends NodeOutput&amp;lt;AgentState&amp;gt; {
    private final String message;

    public ProgressOutput(String node, AgentState state, String message) {
        super(node, state);
        this.message = message;
    }

    public String message() {
        return message;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the node, the dispatcher publishes a message while the returned map still provides the normal state update:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;AsyncNodeActionWithConfig&amp;lt;AgentState&amp;gt; process = (state, config) -&amp;gt; {
    var dispatcher = config.&amp;lt;AgentState, ProgressOutput&amp;gt;customDispatcher();

    dispatcher.dispatchAsync(
        new ProgressOutput(config.nodeId(), state, &quot;Processing documents&quot;));

    // Perform the node's work here.

    return completedFuture(Map.of(&quot;result&quot;, &quot;Documents processed&quot;));
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once this action is registered in a compiled graph, the caller can recognize its progress messages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;graph.stream(GraphInput.noArgs(), RunnableConfig.empty()).forEachAsync( output -&amp;gt; {
        if (output instanceof ProgressOutput progress) {
            System.out.println(progress.message());
        } else {
            System.out.println(&quot;Graph step: &quot; + output.node());
        }
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives us a useful channel for progress notifications, intermediate results, and application-specific events. &lt;strong&gt;Dispatching a custom output does not update graph state.&lt;/strong&gt; If a value must influence routing, be available to later nodes, or become part of a persisted result, include it in the node's returned state update.&lt;/p&gt;
&lt;p&gt;There are two dispatch options. &lt;code&gt;dispatchSync(...)&lt;/code&gt; waits until the stream accepts the value and can throw &lt;code&gt;InterruptedException&lt;/code&gt;; &lt;code&gt;dispatchAsync(...)&lt;/code&gt; submits without waiting. The dispatcher is available during node execution in an active graph stream, including the supported hook context. Keep its use within that lifecycle: accessing it outside has an unpredicable result.&lt;/p&gt;
&lt;p&gt;Custom events are intended for &lt;code&gt;graph.stream(...)&lt;/code&gt; consumers; they do not change the result returned by &lt;code&gt;graph.invoke(...)&lt;/code&gt;. See the &lt;a rel=&quot;nofollow&quot; href=&quot;https://langgraph4j.github.io/langgraph4j/1.9/core/emit-custom-output/&quot;&gt;custom-output tutorial&lt;/a&gt; for the complete contract and a &lt;strong&gt;hook-based&lt;/strong&gt; example.&lt;/p&gt;
&lt;h4 id=&quot;the-streaming-engine-behind-it&quot;&gt;The streaming engine behind it&lt;/h4&gt;
&lt;p&gt;This capability comes with a refactoring of the internal streaming engine around &lt;code&gt;AsyncGeneratorFlow&lt;/code&gt; from &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/bsorrentino/java-async-generator&quot;&gt;&lt;code&gt;async-generator 5.0&lt;/code&gt;&lt;/a&gt; project.&lt;/p&gt;
&lt;p&gt;Ordinary iteration over &lt;code&gt;stream()&lt;/code&gt; remains source-compatible. Applications extending directly streaming generators, supplying a &lt;code&gt;BlockingQueue&lt;/code&gt;, or depending on &lt;code&gt;AsyncGenerator.WithResult&lt;/code&gt; need migration work. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; 👀&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;This refactoring is a groundwork for Reactor Flow support in a future LangGraph4j &lt;code&gt;2.0&lt;/code&gt;; that support is a future direction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;checkpoint-savers-a-richer-execution-lifecycle&quot;&gt;Checkpoint Savers: A richer execution lifecycle&lt;/h3&gt;
&lt;p&gt;Checkpoint persistence is central to long-running agents and Human-in-the-Loop workflows. In &lt;code&gt;1.9&lt;/code&gt;, the changes cover what happens when execution finishes, how released runs can be retrieved, how subgraph savers participate, and how interruptions and failures are recorded. These are the &lt;a rel=&quot;nofollow&quot; href=&quot;https://langgraph4j.github.io/langgraph4j/1.9/whats-new-v1.9&quot;&gt;checkpoint changes&lt;/a&gt; I would review first when upgrading an existing application.&lt;/p&gt;
&lt;h4 id=&quot;completed-runs-release-their-thread-by-default&quot;&gt;Completed runs release their thread by default&lt;/h4&gt;
&lt;p&gt;With a checkpoint saver configured, &lt;strong&gt;a graph that completes normally now releases its active thread automatically&lt;/strong&gt;. Depending on the saver, release archives or tags its checkpoints and removes the active checkpoint set.&lt;/p&gt;
&lt;p&gt;This matters if your application completes a graph, updates the same active thread's state, and executes it again. To retain the previous lifecycle, explicitly disable automatic release when compiling:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;var compileConfig = CompileConfig.builder()
    .checkpointSaver(saver)
    .releaseThread(false) // backward compatible option
    .build();

var graph = workflow.compile(compileConfig);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;An interruption or an exception does not automatically release the thread.&lt;/strong&gt; Interrupted runs remain available for Human-in-the-Loop continuation, and failed runs remain available for the application's recovery strategy. &lt;code&gt;graph.updateState(...)&lt;/code&gt; continues to work in those situations as before.&lt;/p&gt;
&lt;p&gt;The practical upgrade check is to examine what your code does &lt;em&gt;after successful completion&lt;/em&gt;. Any inspection, replay, resume, or manual-release logic that assumes an active checkpoint set needs attention.&lt;/p&gt;
&lt;h4 id=&quot;experimental-versioning-for-released-runs&quot;&gt;Experimental versioning for released runs&lt;/h4&gt;
&lt;p&gt;Checkpoint tags gain optional version information. &lt;code&gt;BaseCheckpointSaver.Tag&lt;/code&gt; exposes &lt;code&gt;threadId()&lt;/code&gt;, &lt;code&gt;version()&lt;/code&gt; as an &lt;code&gt;Optional&amp;lt;Integer&amp;gt;&lt;/code&gt;, &lt;code&gt;checkpoints()&lt;/code&gt;, and &lt;code&gt;lastCheckpoint()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Two lookup methods make released runs accessible:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Optional&amp;lt;Tag&amp;gt; tag(RunnableConfig config, Integer version) throws Exception;

Optional&amp;lt;Tag&amp;gt; lastTag(RunnableConfig config) throws Exception;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;version&lt;/code&gt; argument to &lt;code&gt;tag(...)&lt;/code&gt; is nullable. &lt;code&gt;lastTag(...)&lt;/code&gt; retrieves the latest tag regardless of whether it is versioned.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;GraphResult.asLastCheckpointStateData()&lt;/code&gt; now obtains its data through &lt;code&gt;Tag.lastCheckpoint()&lt;/code&gt;, preserving its observable result. This is useful continuity for callers retrieving the final checkpoint state while the underlying release model evolves.&lt;/p&gt;
&lt;h4 id=&quot;parent-and-subgraph-savers-work-together&quot;&gt;Parent and Subgraph savers work together&lt;/h4&gt;
&lt;p&gt;Subgraphs increasingly carry real agent behavior, so their persistence lifecycle must follow the parent execution.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BaseCheckpointSaver&lt;/code&gt; now supports &lt;code&gt;putSubGraphSaver(...)&lt;/code&gt; to register a subgraph saver and &lt;code&gt;listSubGraphSaver(...)&lt;/code&gt; to retrieve registrations associated with a parent configuration. &lt;code&gt;SubCompiledGraphNodeAction&lt;/code&gt; registers its saver with the parent; &lt;code&gt;AbstractCheckpointSaver.release(...)&lt;/code&gt; then cascades release to the registered subgraph threads.&lt;/p&gt;
&lt;p&gt;If you implement a custom saver, &lt;code&gt;AbstractCheckpointSaver&lt;/code&gt; provides a reference implementation of the registration mechanism. Review that lifecycle when adapting your implementation, especially if your agents contain nested compiled graphs.&lt;/p&gt;
&lt;h4 id=&quot;interruption-and-error-metadata&quot;&gt;Interruption and Error Metadata&lt;/h4&gt;
&lt;p&gt;The saver contract also exposes explicit lifecycle hooks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;void registerInterruption(
    RunnableConfig config,
    InterruptionMetadata interruptionMetadata) throws Exception;

Tag releaseCheckpointsOnError(
    RunnableConfig config,
    Throwable error) throws Exception;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These allow a saver to retain interruption information and distinguish an error release from normal completion. &lt;code&gt;InterruptionMetadata&lt;/code&gt; now also exposes the interruption reason. That gives later analysis more context about why an execution stopped.&lt;/p&gt;
&lt;p&gt;The error-release hook does not change the default described above: an exception leaves the thread available, and the caller chooses its recovery or release strategy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; 👀&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;To avoid leaving lots of unfinished records in the checkpoint persistence layer, we recommend calling &lt;code&gt;saver.releaseOnError(RunnableConfig, Throwable)&lt;/code&gt; within exception handling logic. We are currently considering automating this  in a future release by having the graph execution engine invoke the method whenever an error occurs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The in-memory, file-system, Redis, Postgres, Oracle, MySQL, CockroachDB, DynamoDB, and Hazelcast savers have been aligned with this contract through default implementations. The amount of information retained remains a concern of the specific saver implementation.&lt;/p&gt;
&lt;h4 id=&quot;sqlite-and-postgresql-v2-savers&quot;&gt;SQLite and PostgreSQL V2 savers&lt;/h4&gt;
&lt;p&gt;Both relational integrations add implementations backed by versioned SQL resources:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Integration &lt;/th&gt;&lt;th&gt;New implementation &lt;/th&gt;&lt;th&gt;Existing implementation&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;SQLite &lt;/td&gt;&lt;td&gt;&lt;code&gt;SQLiteSaverV2&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;SQLiteSaver&lt;/code&gt;, for the V1 schema&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;PostgreSQL &lt;/td&gt;&lt;td&gt;&lt;code&gt;PostgresSaverV2&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;PostgresSaver&lt;/code&gt;, for the V1 schema&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The V2 implementations align with the updated release, error, and interruption contract. The original classes remain available for their V1 schemas. Treat adoption of a V2 saver as a persistence change to review against your existing database.&lt;/p&gt;
&lt;h3 id=&quot;other-changes-worth-knowing-before-upgrading&quot;&gt;Other changes worth knowing before upgrading&lt;/h3&gt;
&lt;h4 id=&quot;explicit-graph-inputs&quot;&gt;Explicit Graph Inputs&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;GraphInput&lt;/code&gt; becomes the preferred API for expressing whether an execution is starting or resuming:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;graph.stream(GraphInput.args(Map.of(&quot;input&quot;, &quot;Hello&quot;)), config);
graph.stream(GraphInput.resume(), config);
graph.stream(GraphInput.resume(Map.of(&quot;approval&quot;, &quot;APPROVED&quot;)), config);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;GraphInput.noArgs()&lt;/code&gt; for a new execution without arguments. The older map-based stream and invoke overloads remain available in &lt;code&gt;1.9&lt;/code&gt;, but are deprecated for removal. Moving away from a null map makes the intended operation explicit.&lt;/p&gt;
&lt;h4 id=&quot;diagnostics-and-state-serialization&quot;&gt;Diagnostics and State Serialization&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;RunnableConfig.nodePath()&lt;/code&gt; supplies the current node path for regular nodes and subgraphs, replacing the older partial &lt;code&gt;graphPath()&lt;/code&gt; usage. A node can also intentionally interrupt execution by raising &lt;code&gt;GraphInterruptException&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;State cloning remains enabled by default. &lt;code&gt;RunnableConfig.builder().disableCloneState()&lt;/code&gt; opts out, which can help during early development with objects that are not yet serializable. Exposed state may then reflect later mutations, so consider how consumers retain or modify it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;StateSerializer.declareTransientAttributes(...)&lt;/code&gt; excludes named attributes from serialized data and restores them from the same serializer's in-memory transient storage. Those values do not survive a restart, a different process, or a new serializer instance. Keep anything needed for durable resume in persisted state. &lt;code&gt;GsonStateSerializer&lt;/code&gt; is deprecated for removal; move toward Jackson-based serialization.&lt;/p&gt;
&lt;h4 id=&quot;agent-infrastructure&quot;&gt;Agent infrastructure&lt;/h4&gt;
&lt;p&gt;The experimental core skills API introduces &lt;code&gt;SkillSource&lt;/code&gt;, &lt;code&gt;SkillPath&lt;/code&gt;, and &lt;code&gt;SkillParser&lt;/code&gt;. &lt;/p&gt;
&lt;p&gt;Spring AI builds on this with &lt;code&gt;SubAgent&lt;/code&gt;, &lt;code&gt;CustomSubAgent&lt;/code&gt;, &lt;code&gt;SkilledReactSubAgent&lt;/code&gt;, and &lt;code&gt;SkillResource&lt;/code&gt;, making compiled agents reusable as tools. I covered the pattern in &lt;a rel=&quot;nofollow&quot; href=&quot;https://bsorrentino.github.io/bsorrentino/ai/2026/04/28/LangGraph4j-SubAgent.html&quot;&gt;Skill-Based Sub-Agents with LangGraph4j and Spring AI&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;langgraph4j-studio-gets-a-visual-refresh&quot;&gt;LangGraph4j Studio gets a visual refresh&lt;/h3&gt;
&lt;h4 id=&quot;studio-1-8&quot;&gt;Studio 1.8&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=10733277276186610415&quot; alt=&quot;LangGraph4j Studio 1.8&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;studio-1-9&quot;&gt;Studio 1.9&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=17852363960302728394&quot; alt=&quot;LangGraph4j Studio 1.9&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the new Studio the active node receives a blue highlight and a loading indicator, making execution easier to follow at a glance.&lt;/p&gt;
&lt;h3 id=&quot;langgraph4j-dsl&quot;&gt;LangGraph4j DSL&lt;/h3&gt;
&lt;p&gt;The release also adds &lt;code&gt;langgraph4j-dsl&lt;/code&gt;, whose &lt;code&gt;JsonDslGenerator&lt;/code&gt; exports graphs, including parallel nodes and nested subgraphs, as JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String json = compiledGraph.reduce(new JsonDslGenerator&amp;lt;&amp;gt;());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This representation has been used for the Studio refactoring toward &lt;a rel=&quot;nofollow&quot; href=&quot;https://reactflow.dev&quot;&gt;React Flow&lt;/a&gt; library.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-s-next-for-langgraph4j&quot;&gt;What's Next for LangGraph4j&lt;/h3&gt;
&lt;p&gt;The next phase of development will focus on three areas. These are roadmap priorities, with scope and delivery to evolve as implementation progresses.&lt;/p&gt;
&lt;h4 id=&quot;persist-more-of-the-execution-context&quot;&gt;Persist more of the execution context&lt;/h4&gt;
&lt;p&gt;I want to improve graph execution-context persistence so that it supports monitoring and post-processing analysis more effectively. The checkpoint lifecycle work in &lt;code&gt;1.9&lt;/code&gt; is a step in that direction: understanding completed, interrupted, and failed runs is essential when agentic workflows move into production.&lt;/p&gt;
&lt;p&gt;The aim is to make retained execution information more useful for understanding behavior and for subsequent analysis and processing.&lt;/p&gt;
&lt;h4 id=&quot;redesign-parallel-node-execution&quot;&gt;Redesign parallel node execution&lt;/h4&gt;
&lt;p&gt;Another priority is to redesign the parallel-node implementation, removing current limitations and improving efficiency. As workflows grow, parallel branches need to become easier to compose and manage.&lt;/p&gt;
&lt;p&gt;This is upcoming work. Applications using &lt;code&gt;1.9&lt;/code&gt; should continue to account for the currently documented parallel-execution limitations.&lt;/p&gt;
&lt;h4 id=&quot;improve-built-in-agents-for-spring-ai-and-langchain4j&quot;&gt;Improve built-in Agents for Spring AI and LangChain4j&lt;/h4&gt;
&lt;p&gt;Finally, I want to enhance the built-in agent infrastructure for both &lt;strong&gt;Spring AI&lt;/strong&gt; and &lt;strong&gt;LangChain4j&lt;/strong&gt;. The skills and sub-agent work demonstrates how much can be built on top of graph execution; the goal is to make those building blocks more useful and easier to compose across both integrations.&lt;/p&gt;
&lt;h3 id=&quot;try-the-release&quot;&gt;Try the Release&lt;/h3&gt;
&lt;p&gt;Update the LangGraph4j BOM to &lt;code&gt;1.9.0&lt;/code&gt; to keep module versions aligned:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;dependencyManagement&amp;gt;
  &amp;lt;dependencies&amp;gt;
    &amp;lt;dependency&amp;gt;
      &amp;lt;groupId&amp;gt;org.bsc.langgraph4j&amp;lt;/groupId&amp;gt;
      &amp;lt;artifactId&amp;gt;langgraph4j-bom&amp;lt;/artifactId&amp;gt;
      &amp;lt;version&amp;gt;1.9.0&amp;lt;/version&amp;gt;
      &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
      &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
    &amp;lt;/dependency&amp;gt;
  &amp;lt;/dependencies&amp;gt;
&amp;lt;/dependencyManagement&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h3&gt;
&lt;p&gt;For an existing &lt;code&gt;1.8.x&lt;/code&gt; application, start with the &lt;a rel=&quot;nofollow&quot; href=&quot;https://langgraph4j.github.io/langgraph4j/1.9/whats-new-v1.9&quot;&gt;migration guide&lt;/a&gt;, review thread release and stored checkpoint compatibility, then exercise interruption, resume, and completion paths with your chosen saver.&lt;/p&gt;
&lt;p&gt;I'm interested in feedback from real workflows, especially around custom progress output and checkpoint management. Check out &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/langgraph4j/langgraph4j&quot;&gt;LangGraph4j&lt;/a&gt;, try the release, and let me know what would help your next agentic application. If you find the project useful, leave a star ⭐️ and... happy AI coding! 👋&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27744/langgraph4j-1-9-0-whats-new-and-the-roadmap</guid>
<pubDate>Sat, 19 Sep 2026 13:05:32 +0000</pubDate>
</item>
<item>
<title>Managed Agent Infrastructure: What Changes When the Runtime Is Handled For You</title>
<link>https://coderlegion.com/24657/managed-agent-infrastructure-what-changes-when-the-runtime-is-handled-for-you</link>
<description>&lt;p&gt;Managed agent frameworks address a specific infrastructure challenge: streams that don't timeout, memory that persists across sessions, and auth that prevents data leakage. Building one that handles all three in production is where most teams stall.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Shift From DIY Agent Plumbing to Managed Runtimes&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When you build an AI agent yourself, roughly half the engineering effort has nothing to do with the agent's actual behavior - it goes into scaffolding: wiring up a streaming layer, managing sandboxed code execution (an isolated environment so agent-generated code can't touch your production systems), persisting memory between conversations, and handling authentication so users only see what they're supposed to see.&lt;/p&gt;
&lt;p&gt;According to LangChain's framework documentation, managed agent frameworks provide infrastructure as defaults rather than something you assemble. The agent runtime is the execution environment that actually &lt;em&gt;runs&lt;/em&gt; your agent loop, handles retries, streams tokens back to the user, and checkpoints state. This comes pre-built. You define the tools, the model, and the logic. The platform handles how that runs reliably at scale.&lt;/p&gt;
&lt;p&gt;For teams where the AI engineer and the product builder are the same person, this shift is particularly valuable. If you're a forward deployed engineer shipping an internal tool, or a data scientist turning a prototype into something real users touch, you don't have time to maintain bespoke streaming infrastructure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here's roughly what the shift looks like in practice. A DIY setup for a simple memory-enabled agent in LangGraph might require you to manually wire a checkpointer (the component that saves conversation state between turns):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent

checkpointer = MemorySaver()
agent = create_react_agent(
 model=llm,
 tools=tools,
 checkpointer=checkpointer
)
config = {&quot;configurable&quot;: {&quot;thread_id&quot;: &quot;user-session-42&quot;}}
result = agent.invoke({&quot;messages&quot;: [...]}, config=config)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a managed setup, the checkpointer, the streaming layer, and the sandbox are provisioned for you - you hand off &lt;code&gt;tools&lt;/code&gt; and &lt;code&gt;model&lt;/code&gt;, declare the agent, and deploy. The operational surface you maintain shrinks considerably. You still own the agent's behavior; you just don't own the plumbing it runs through.&lt;/p&gt;
&lt;p&gt;The tradeoff is real: managed runtimes introduce platform dependency and can constrain how you debug or customize the execution loop. For teams that need full control over retry logic or want to inspect every state transition, self-hosted is still the right call.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Agent scaffolding (streaming, memory, sandboxing, auth) consumes a disproportionate share of build time&lt;/strong&gt; - managed runtimes specifically target that overhead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The agent's behavior stays yours&lt;/strong&gt; - tools, prompts, model choice, and logic remain in your control; the runtime layer is what's abstracted.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Managed isn't always better&lt;/strong&gt; - teams needing deep observability or custom execution control may find self-hosted LangGraph or similar frameworks give more flexibility.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you've shipped an agent to production, which layer gave you the most unexpected trouble - memory, streaming, auth, or something else entirely?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Sources referenced: LangChain Blog - &quot;Why managed agents are the next big thing in agent building&quot;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24657/managed-agent-infrastructure-what-changes-when-the-runtime-is-handled-for-you</guid>
<pubDate>Fri, 14 Aug 2026 22:11:13 +0000</pubDate>
</item>
<item>
<title>From RAG to Agentic RAG. How I Added LangGraph to My Local</title>
<link>https://coderlegion.com/23826/from-rag-to-agentic-rag-how-i-added-langgraph-to-my-local</link>
<description>&lt;p&gt;In my &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/23449/i-built-a-local-rag-assistant-with-ollama-chromadb-and-langchain&quot;&gt;previous article&lt;/a&gt;, I built a fully local RAG assistant Ollama, ChromaDB, LangChain, all running in Docker. It answered technical support questions by searching through documentation and citing sources. It worked.&lt;/p&gt;
&lt;p&gt;But after using it for a while, I noticed something uncomfortable: &lt;strong&gt;it treated every question the same way&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Ask it &lt;em&gt;&quot;how to close monthly payroll?&quot;&lt;/em&gt; it searches the docs. Fine.&lt;br&gt;
Ask it &lt;em&gt;&quot;the server crashes at startup&quot;&lt;/em&gt; it also searches the docs. Less fine.&lt;br&gt;
Ask it something completely outside the documentation it searches the docs. Useless.&lt;/p&gt;
&lt;p&gt;A real support technician doesn't do that. They first &lt;em&gt;assess&lt;/em&gt; the situation, then decide what to do: look it up, run a diagnosis, or escalate to a human. My RAG had no such judgment.&lt;/p&gt;
&lt;p&gt;That's what this article is about how I evolved the system into an &lt;strong&gt;Agentic AI architecture&lt;/strong&gt; using LangGraph, where the assistant first decides &lt;em&gt;which strategy to use&lt;/em&gt;, then acts accordingly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-core-limitation-of-classic-rag&quot;&gt;The Core Limitation of Classic RAG&lt;/h3&gt;
&lt;p&gt;Classic RAG is a linear pipeline. Every query follows the exact same path:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Question → Embed → Retrieve → Prompt → LLM → Answer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No branching. No decision-making. No memory between steps.&lt;/p&gt;
&lt;p&gt;This works perfectly for procedural questions where the answer lives in the docs. But technical support involves at least three distinct scenarios:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Scenario &lt;/th&gt;&lt;th&gt;Example &lt;/th&gt;&lt;th&gt;Best strategy&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Procedural question &lt;/td&gt;&lt;td&gt;&quot;How do I create an account?&quot; &lt;/td&gt;&lt;td&gt;Search documentation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Known error code &lt;/td&gt;&lt;td&gt;&quot;ERR-COMP-001 appears&quot; &lt;/td&gt;&lt;td&gt;Lookup error database&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Unknown incident &lt;/td&gt;&lt;td&gt;&quot;Server crashes, no idea why&quot; &lt;/td&gt;&lt;td&gt;Diagnose + escalate if needed&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A single RAG pipeline handles the first case well and the other two poorly. The solution is to add a layer of reasoning &lt;em&gt;before&lt;/em&gt; retrieval.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-agentic-ai-adds&quot;&gt;What Agentic AI Adds&lt;/h3&gt;
&lt;p&gt;The shift from RAG to Agentic AI comes down to one thing: &lt;strong&gt;the system plans before it acts&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Instead of one fixed pipeline, you have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Question
    ↓
Classifier (what kind of question is this?)
    ↓
    ├── Procedural → RAG Agent (search docs)
    ├── Error code → Diagnostic Agent (lookup + LLM analysis)
    └── Complex    → Diagnostic Agent → Escalation Agent (generate ticket)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each branch is an &lt;strong&gt;agent&lt;/strong&gt;; an autonomous function that receives a shared state, does its job, updates the state, and returns it. LangGraph connects these agents into a directed graph with conditional routing between them.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-shared-state-foundation-of-langgraph&quot;&gt;The Shared State Foundation of LangGraph&lt;/h3&gt;
&lt;p&gt;Before building agents, you define the state they all share. Think of it as a case file that every agent reads and updates:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# src/agents/state.py

from typing import TypedDict, List, Annotated, Sequence
from langchain_core.messages import BaseMessage
import operator

class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]
    question: str          # Original question never changes
    plan: List[str]
    current_step: int
    past_steps: List[str]  # Audit trail of what each agent did
    response: str
    next_agent: str
    niveau_support: int    # 1=resolved, 2=partial, 3=needs human
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;niveau_support&lt;/code&gt; field is key it's how agents communicate urgency to each other. If an agent sets it to 3, the orchestrator escalates automatically.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Annotated[Sequence[BaseMessage], operator.add]&lt;/code&gt; means messages accumulate — each agent appends rather than overwrites. You get a full conversation trace at the end.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;agent-1-the-rag-agent&quot;&gt;Agent 1 The RAG Agent&lt;/h3&gt;
&lt;p&gt;The pipeline from my previous article, repackaged as an autonomous agent:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# src/agents/rag_agent.py

def rag_agent(state: AgentState) -&amp;gt; AgentState:
    &quot;&quot;&quot;Searches the documentation and generates a grounded answer.&quot;&quot;&quot;

    docs = vectorstore.similarity_search(state[&quot;question&quot;], k=3)

    if not docs:
        return {
            **state,
            &quot;messages&quot;: list(state[&quot;messages&quot;]) + [
                AIMessage(content=&quot;[RAG] No relevant document found.&quot;)
            ],
            &quot;past_steps&quot;: state[&quot;past_steps&quot;] + [&quot;RAG: no results&quot;],
            &quot;niveau_support&quot;: 3   # Nothing found — escalate
        }

    context = &quot;\n\n&quot;.join([
        f&quot;[Source: {d.metadata.get('module', '?')}]\n{d.page_content}&quot;
        for d in docs
    ])

    answer = llm.invoke(
        f&quot;Answer from context only.\nContext: {context}\nQuestion: {state['question']}&quot;
    )

    return {
        **state,
        &quot;messages&quot;: list(state[&quot;messages&quot;]) + [AIMessage(content=f&quot;[RAG] {answer}&quot;)],
        &quot;past_steps&quot;: state[&quot;past_steps&quot;] + [&quot;RAG: documentation search completed&quot;],
        &quot;niveau_support&quot;: 1
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The critical difference from the classic RAG: the agent returns an &lt;strong&gt;updated state&lt;/strong&gt;, not just an answer. Every agent follows this same pattern — receive state, do work, return updated state.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;agent-2-the-diagnostic-agent&quot;&gt;Agent 2 The Diagnostic Agent&lt;/h3&gt;
&lt;p&gt;Handles error codes and technical incidents with two modes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# src/agents/diagnostic_agent.py

KNOWN_ERRORS = {
    &quot;ERR-COMP-001&quot;: {
        &quot;description&quot;: &quot;Debit/credit imbalance&quot;,
        &quot;solution&quot;: &quot;Check Total Debit = Total Credit. Go to Accounting &amp;gt; Journal Entry.&quot;
    },
    &quot;ERR-PAIE-042&quot;: {
        &quot;description&quot;: &quot;Payroll line missing rate&quot;,
        &quot;solution&quot;: &quot;Go to Settings &amp;gt; Payroll Lines, fill in the missing rate.&quot;
    },
}

def diagnostic_agent(state: AgentState) -&amp;gt; AgentState:
    question = state[&quot;question&quot;]
    found_codes = [code for code in KNOWN_ERRORS if code.lower() in question.lower()]

    if found_codes:
        # Mode 1 : known error: instant answer, no LLM call needed
        responses = [
            f&quot;Code {code}: {KNOWN_ERRORS[code]['description']}\n&quot;
            f&quot;Solution: {KNOWN_ERRORS[code]['solution']}&quot;
            for code in found_codes
        ]
        return {
            **state,
            &quot;messages&quot;: list(state[&quot;messages&quot;]) + [
                AIMessage(content=f&quot;[Diagnostic] {chr(10).join(responses)}&quot;)
            ],
            &quot;past_steps&quot;: state[&quot;past_steps&quot;] + [&quot;Diagnostic: known error, resolved&quot;],
            &quot;niveau_support&quot;: 1
        }

    # Mode 2 : unknown error: LLM analysis
    answer = llm.invoke(
        f&quot;Analyze this technical issue. If unsure, say so.\nIssue: {question}&quot;
    )
    return {
        **state,
        &quot;messages&quot;: list(state[&quot;messages&quot;]) + [AIMessage(content=f&quot;[Diagnostic] {answer}&quot;)],
        &quot;past_steps&quot;: state[&quot;past_steps&quot;] + [&quot;Diagnostic: LLM analysis, uncertain&quot;],
        &quot;niveau_support&quot;: 2
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The known error lookup runs in milliseconds without touching the LLM; a meaningful performance gain for common incidents.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;agent-3-the-escalation-agent&quot;&gt;Agent 3 : The Escalation Agent&lt;/h3&gt;
&lt;p&gt;Called only when &lt;code&gt;niveau_support &amp;gt;= 3&lt;/code&gt;. It doesn't try to answer; it generates a structured ticket with the full diagnostic history already attached:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# src/agents/escalade_agent.py

def escalade_agent(state: AgentState) -&amp;gt; AgentState:
    ticket = f&quot;&quot;&quot;
=== SUPPORT TICKET — LEVEL 3 ===
Date     : {datetime.now().strftime(&quot;%Y-%m-%d %H:%M&quot;)}
Priority : HIGH

ISSUE:
{state[&quot;question&quot;]}

DIAGNOSTIC STEPS ALREADY TAKEN:
{chr(10).join(state[&quot;past_steps&quot;])}

CONCLUSION: Human intervention required.
================================&quot;&quot;&quot;

    return {
        **state,
        &quot;messages&quot;: list(state[&quot;messages&quot;]) + [AIMessage(content=ticket)],
        &quot;past_steps&quot;: state[&quot;past_steps&quot;] + [&quot;Escalation: level 3 ticket generated&quot;],
        &quot;niveau_support&quot;: 3
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is what separates a real support system from a chatbot; it knows when to stop and hand off, with context already attached so the user doesn't have to repeat themselves.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-orchestrator-wiring-it-all-together&quot;&gt;The Orchestrator : Wiring It All Together&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# src/agents/orchestrator.py

def classifier(state: AgentState) -&amp;gt; str:
    question = state[&quot;question&quot;].lower()

    if re.search(r'err-\w+-\d+', question):
        return &quot;diagnostic&quot;

    problem_words = [&quot;error&quot;, &quot;crash&quot;, &quot;bug&quot;, &quot;not working&quot;, &quot;fails&quot;, &quot;problem&quot;]
    if any(word in question for word in problem_words):
        return &quot;diagnostic&quot;

    return &quot;rag&quot;  # Default for procedural questions


def should_escalate(state: AgentState) -&amp;gt; str:
    return &quot;escalade&quot; if state.get(&quot;niveau_support&quot;, 1) &amp;gt;= 3 else END


def create_orchestrator():
    workflow = StateGraph(AgentState)

    workflow.add_node(&quot;rag_agent&quot;, rag_agent)
    workflow.add_node(&quot;diagnostic_agent&quot;, diagnostic_agent)
    workflow.add_node(&quot;escalade_agent&quot;, escalade_agent)

    workflow.set_conditional_entry_point(
        classifier,
        {&quot;rag&quot;: &quot;rag_agent&quot;, &quot;diagnostic&quot;: &quot;diagnostic_agent&quot;}
    )

    workflow.add_edge(&quot;rag_agent&quot;, END)
    workflow.add_conditional_edges(
        &quot;diagnostic_agent&quot;,
        should_escalate,
        {&quot;escalade&quot;: &quot;escalade_agent&quot;, END: END}
    )
    workflow.add_edge(&quot;escalade_agent&quot;, END)

    return workflow.compile()
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;seeing-it-in-action&quot;&gt;Seeing It In Action&lt;/h3&gt;
&lt;p&gt;Three questions, three different paths:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Procedural question&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;Input  : &quot;How to close monthly payroll?&quot;
Path   : classifier → rag_agent → END
Output : agents_used: [&quot;RAG: documentation search completed&quot;]
         niveau: 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Known error code&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;Input  : &quot;ERR-COMP-001 appears on journal validation&quot;
Path   : classifier → diagnostic_agent → END
Output : agents_used: [&quot;Diagnostic: known error, resolved&quot;]
         niveau: 1  (no LLM call, instant response)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Unknown incident&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;Input  : &quot;Server crashes at startup, no idea why&quot;
Path   : classifier → diagnostic_agent → escalade_agent → END
Output : agents_used: [&quot;Diagnostic: LLM analysis&quot;, &quot;Escalation: ticket generated&quot;]
         niveau: 3
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-would-do-differently&quot;&gt;What I Would Do Differently&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The classifier is fragile.&lt;/strong&gt; Keyword matching breaks on edge cases &lt;em&gt;&quot;I don't have a problem, just need help configuring payroll&quot;&lt;/em&gt; contains &quot;problem&quot; and routes to diagnostic instead of RAG. A small LLM classification call, or a fine-tuned classifier, would be more robust.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agents reload the embedding model on every call.&lt;/strong&gt; Instantiating &lt;code&gt;HuggingFaceEmbeddings&lt;/code&gt; on each agent invocation adds unnecessary overhead. Initialize once at startup and pass the instance through the state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The escalation threshold is hardcoded.&lt;/strong&gt; A feedback loop where user ratings influence the confidence threshold over time would make this genuinely adaptive that's what reinforcement learning adds in the next phase of this project.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;rag-vs-agentic-ai-when-to-use-which&quot;&gt;RAG vs Agentic AI : When to Use Which&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Criteria        &lt;/th&gt;&lt;th&gt;Classic RAG              &lt;/th&gt;&lt;th&gt;Agentic AI&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Best for       &lt;/td&gt;&lt;td&gt;Uniform question types   &lt;/td&gt;&lt;td&gt;Mixed question types&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Complexity     &lt;/td&gt;&lt;td&gt;Low                      &lt;/td&gt;&lt;td&gt;Medium-High&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Latency        &lt;/td&gt;&lt;td&gt;One LLM call             &lt;/td&gt;&lt;td&gt;One or more LLM calls&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Flexibility    &lt;/td&gt;&lt;td&gt;Fixed pipeline           &lt;/td&gt;&lt;td&gt;Extensible — add agents without rewriting&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;When to choose &lt;/td&gt;&lt;td&gt;Start here               &lt;/td&gt;&lt;td&gt;When RAG fails on edge cases&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;My honest take: &lt;strong&gt;start with RAG&lt;/strong&gt;. Get it working, understand its failure modes, then add agents where you actually need branching logic. Adding LangGraph too early is over-engineering.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Design your &lt;code&gt;AgentState&lt;/code&gt; first.&lt;/strong&gt; It's the contract between all components. Changing it mid-project means updating every agent get it right upfront.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agents should do one thing.&lt;/strong&gt; The moment an agent tries to retrieve &lt;em&gt;and&lt;/em&gt; diagnose, it becomes untestable and unroutable. Keep them single-responsibility.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;past_steps&lt;/code&gt; is your audit trail.&lt;/strong&gt; When a ticket escalates to a human, they see exactly what the system tried. Small design choice, professional result.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The classifier is your most important and most brittle piece of code.&lt;/strong&gt; Test it with as many edge cases as you can before trusting it.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;These two articles cover Phases 1 and 2 of my internship project. The logical next step is a reinforcement learning feedback loop; user ratings adjusting the classifier's routing decisions over time; which I may write about once it's implemented and tested.&lt;/p&gt;
&lt;p&gt;I'm a Master's student in AI &amp;amp; Big Data, building these systems with open-source tools and no GPU. If you're working on multi-agent systems or ran into similar design decisions; especially around the classifier or state design; I'd love to hear your approach in the comments.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23826/from-rag-to-agentic-rag-how-i-added-langgraph-to-my-local</guid>
<pubDate>Thu, 30 Jul 2026 19:49:17 +0000</pubDate>
</item>
<item>
<title>The cost of a serverless agent isn't the Lambda. It's the loop.</title>
<link>https://coderlegion.com/22873/the-cost-of-a-serverless-agent-isnt-the-lambda-its-the-loop</link>
<description>&lt;p&gt;When I moved Wingman onto Lambda, I assumed the thing I'd have to watch was compute duration. That's the serverless mental model everyone starts with: you pay per millisecond, so keep the function fast and cheap.&lt;/p&gt;
&lt;p&gt;That model was wrong for an agent. The Lambda bill turned out to be the boring, predictable part. The two things that actually move cost and latency are the cold start and the retry loop, and neither of them shows up where you'd look first.&lt;/p&gt;
&lt;p&gt;Here's what the code taught me.&lt;/p&gt;
&lt;h3 id=&quot;the-setup-what-wingman-actually-is&quot;&gt;The setup: what Wingman actually is&lt;/h3&gt;
&lt;p&gt;Wingman is a LangGraph worker-evaluator agent. A worker node (running &lt;code&gt;gpt-4o-mini&lt;/code&gt;) does the task. An evaluator node (also &lt;code&gt;gpt-4o-mini&lt;/code&gt;) grades the answer against a success criteria. If the criteria isn't met, control routes back to the worker for another attempt, up to five times.&lt;/p&gt;
&lt;p&gt;It runs as a container image on Lambda, fronted by API Gateway and CloudFront, with session history in DynamoDB. The compute is stateless: every request rebuilds the conversation from DynamoDB, so there's no persistent checkpointer to worry about across cold and warm starts. I wrote about that state design in an earlier post. This one is about what it costs to run.&lt;/p&gt;
&lt;h3 id=&quot;cold-start-1-the-image&quot;&gt;Cold start #1: the image&lt;/h3&gt;
&lt;p&gt;A Lambda container cold start begins by unpacking the image. Wingman's image carries langchain, langgraph, langchain-community, langchain-experimental, boto3, plus reportlab and sendgrid for the agent's tools. That's not a small pile of Python.&lt;/p&gt;
&lt;p&gt;The one decision I'm glad I made is in the Dockerfile. The project has a &lt;code&gt;local&lt;/code&gt; dependency group with playwright, gradio, and uvicorn, and a &lt;code&gt;dev&lt;/code&gt; group on top of that. None of those belong in Lambda. So the build exports only the core deps and drops the rest:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;RUN uv export \
      --no-dev \
      --no-group dev \
      --no-group local \
      --format requirements-txt \
      -o /tmp/requirements.txt \
    &amp;amp;&amp;amp; pip install -r /tmp/requirements.txt --no-cache-dir \
      --target &quot;${LAMBDA_TASK_ROOT}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Playwright alone would have pulled a browser runtime into the image. Keeping it in the &lt;code&gt;local&lt;/code&gt; group means it exists for running the agent on my laptop and never ships to Lambda. Smaller image, faster unpack, faster cold start. This is the kind of thing you only notice you did right when you compare it to the version where you didn't.&lt;/p&gt;
&lt;h3 id=&quot;cold-start-2-the-init-you-pay-for-on-import&quot;&gt;Cold start #2: the init you pay for on import&lt;/h3&gt;
&lt;p&gt;The bigger cold-start cost is in the handler, and it's easy to miss because it happens at import time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# backend/main.py — runs once per new execution environment
wingman = Wingman()
wingman.setup()

_dynamodb = boto3.resource(&quot;dynamodb&quot;, region_name=os.getenv(&quot;AWS_REGION&quot;, &quot;us-east-1&quot;))
_table = _dynamodb.Table(_dynamodb_table_name)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;setup()&lt;/code&gt; isn't cheap. It builds the tool list, constructs two &lt;code&gt;ChatOpenAI&lt;/code&gt; clients, binds tools to the worker, wraps the evaluator in structured output, and compiles the LangGraph:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def setup(self):
    self.tools = get_tools()
    worker_llm = ChatOpenAI(model=&quot;gpt-4o-mini&quot;)
    self.worker_llm_with_tools = worker_llm.bind_tools(self.tools)
    evaluator_llm = ChatOpenAI(model=&quot;gpt-4o-mini&quot;)
    self.evaluator_llm_with_output = evaluator_llm.with_structured_output(EvaluatorOutput)
    self._build_graph()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Putting this at module level is the right call. It runs once when a new execution environment spins up, then every warm invocation reuses the same compiled graph and the same clients. If I'd built the graph inside the request handler instead, I'd be paying graph compilation on every single &lt;code&gt;/api/chat&lt;/code&gt; call, warm or not. Module-level init trades a heavier cold start for near-zero per-request setup, which is the trade you want when warm invocations vastly outnumber cold ones.&lt;/p&gt;
&lt;p&gt;The lever for cold-start speed is memory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &quot;aws_lambda_function&quot; &quot;backend&quot; {
  timeout     = 300   # 5 minutes
  memory_size = 1024
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On Lambda, CPU scales with memory. At 1024MB you get roughly a full vCPU, so bumping memory doesn't just buy RAM headroom, it makes that import-time init finish faster. Memory is a latency knob here, not only a capacity one.&lt;/p&gt;
&lt;h3 id=&quot;the-real-cost-the-retry-loop-not-the-duration&quot;&gt;The real cost: the retry loop, not the duration&lt;/h3&gt;
&lt;p&gt;Now the part that surprised me.&lt;/p&gt;
&lt;p&gt;Look at the timeout: 300 seconds. Lambda's max is 900. That's a generous ceiling for a function whose real job takes a few seconds. So why so high? Because the retry loop can run the worker up to five times, and each turn is a full round of model calls plus whatever tools the worker decides to invoke.&lt;/p&gt;
&lt;p&gt;Here's the accounting that matters. &lt;code&gt;turn_count&lt;/code&gt; only increments in the evaluator:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def evaluator(self, state: State) -&amp;gt; Dict[str, Any]:
    ...
    return {
        ...
        &quot;turn_count&quot;: state.get(&quot;turn_count&quot;, 0) + 1,
    }

def route_based_on_evaluation(self, state: State) -&amp;gt; str:
    if state[&quot;success_criteria_met&quot;] or state[&quot;user_input_needed&quot;]:
        return &quot;END&quot;
    if state.get(&quot;turn_count&quot;, 0) &amp;gt;= self.MAX_TURNS:
        return &quot;END&quot;
    return &quot;worker&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/1awc8zakbl6gl0wp9nnn.png&quot; alt=&quot;Wingman architecture on AWS Lambda showing the cold-start init path (ECR image unpack into module-level Wingman.setup building two gpt-4o-mini clients and compiling the LangGraph) and the per-request path (client through CloudFront and API Gateway with its 29-second cap, or the Lambda Function URL for long tasks, into FastAPI, DynamoDB session history, and the worker-evaluator retry loop that runs up to 5 times).&quot;&gt;&lt;/p&gt;
&lt;p&gt;So a single &lt;code&gt;/api/chat&lt;/code&gt; request is not one model call. In the worst case it's five worker calls, five evaluator calls, and every tool call the worker makes in between. The Lambda duration for that is trivial to reason about and cheap. The token cost is the thing that swings, and it swings by a factor of five depending on how many times the evaluator sends the work back.&lt;/p&gt;
&lt;p&gt;That reframed the whole cost model for me. Moving to stateless compute on Lambda made the infrastructure cost flat and predictable. What it did was push all the cost variance out of the infrastructure and into the model loop. The bill I have to watch isn't Lambda-seconds. It's tokens times retries.&lt;/p&gt;
&lt;h3 id=&quot;the-29-second-cliff&quot;&gt;The 29-second cliff&lt;/h3&gt;
&lt;p&gt;One more thing the config quietly documents. API Gateway has a hard 29-second integration timeout. A five-turn loop with tool calls can blow past that even though the Lambda itself is happily configured for 300 seconds. So the request dies at the gateway while the function is still working.&lt;/p&gt;
&lt;p&gt;The fix is in the Terraform: a Lambda Function URL that bypasses API Gateway entirely for long tasks.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &quot;aws_lambda_function_url&quot; &quot;backend&quot; {
  function_name      = aws_lambda_function.backend.function_name
  authorization_type = &quot;NONE&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Short chats go through CloudFront and API Gateway. Long agent runs go straight to the Function URL and get the full timeout budget. It works, but &lt;code&gt;authorization_type = &quot;NONE&quot;&lt;/code&gt; and &lt;code&gt;allow_origins = [&quot;*&quot;]&lt;/code&gt; are both flagged in the code as things to tighten after the first deploy, and they're still open. An unauthenticated public URL that invokes a model loop is a bill waiting to happen if someone finds it. That one's on my list.&lt;/p&gt;
&lt;h3 id=&quot;what-i-d-do-differently&quot;&gt;What I'd do differently&lt;/h3&gt;
&lt;p&gt;The retry loop has no token budget. It caps at five turns, but a &quot;turn&quot; can be an expensive worker call with several tool invocations, and nothing stops a run from burning far more tokens than the answer is worth. A turn cap is a crude proxy for a cost cap. If I rebuilt this, the evaluator would carry a token budget alongside &lt;code&gt;turn_count&lt;/code&gt;, and the loop would stop on whichever it hit first.&lt;/p&gt;
&lt;h3 id=&quot;takeaway&quot;&gt;Takeaway&lt;/h3&gt;
&lt;p&gt;For a serverless agent, Lambda duration is the cheap, predictable line on the bill. The cost that actually moves lives in the cold-start init you pay on import and the retry loop you pay per request, so budget those, not the milliseconds.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22873/the-cost-of-a-serverless-agent-isnt-the-lambda-its-the-loop</guid>
<pubDate>Fri, 17 Jul 2026 05:46:41 +0000</pubDate>
</item>
<item>
<title>Building a worker-evaluator retry loop in LangGraph (and where it bites you on Lambda)</title>
<link>https://coderlegion.com/22871/building-a-worker-evaluator-retry-loop-in-langgraph-and-where-it-bites-you-on-lambda</link>
<description>&lt;p&gt;Most agent demos stop at &quot;the model called a tool and gave an answer.&quot; That answer is often wrong, and nothing in the loop notices. I wanted an agent that checks its own work against a success criteria before returning, and retries when it falls short. That is the whole idea behind Wingman: a worker that does the task, and a separate evaluator that decides whether the task is actually done.&lt;/p&gt;
&lt;p&gt;Here is how the loop is built, the one decision that shaped everything, and the part that broke once it ran on Lambda behind API Gateway.&lt;/p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The problem&lt;/h3&gt;
&lt;p&gt;A single-pass agent has no idea when it failed. It calls a tool, produces text, and returns. If the output misses the point, the user finds out, not the system.&lt;/p&gt;
&lt;p&gt;I wanted a gate. Something that reads the assistant's last response, compares it against a stated success criteria, and either accepts it or sends the worker back to try again with feedback. Two roles, not one. The worker produces. The evaluator judges.&lt;/p&gt;
&lt;p&gt;The catch is that a retry loop with no ceiling is a bill waiting to happen. So the loop needs a hard stop, and it needs to know the difference between &quot;the assistant needs to try again&quot; and &quot;the assistant is stuck and should ask the user.&quot;&lt;/p&gt;
&lt;h3 id=&quot;the-approach-and-the-key-decision&quot;&gt;The approach and the key decision&lt;/h3&gt;
&lt;p&gt;The graph has three nodes: &lt;code&gt;worker&lt;/code&gt;, &lt;code&gt;tools&lt;/code&gt;, and &lt;code&gt;evaluator&lt;/code&gt;. The state carries the conversation, the success criteria, the latest feedback, two boolean flags, and a turn counter.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class State(TypedDict):
    messages: Annotated[List[Any], add_messages]
    success_criteria: str
    feedback_on_work: Optional[str]
    success_criteria_met: bool
    user_input_needed: bool
    turn_count: int
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The worker runs first. If its last message contains tool calls, the router sends it to the tool node and back. If not, it goes to the evaluator.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def worker_router(self, state: State) -&amp;gt; str:
    last = state[&quot;messages&quot;][-1]
    return &quot;tools&quot; if (hasattr(last, &quot;tool_calls&quot;) and last.tool_calls) else &quot;evaluator&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key decision is making the evaluator a structured-output call, not a free-text one. It returns a typed object, so the routing logic reads clean booleans instead of parsing prose.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class EvaluatorOutput(BaseModel):
    feedback: str = Field(description=&quot;Feedback on the assistant's response&quot;)
    success_criteria_met: bool = Field(description=&quot;Whether the success criteria have been met&quot;)
    user_input_needed: bool = Field(
        description=&quot;True if more input is needed from the user, or the assistant is stuck&quot;
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both the worker and the evaluator run on &lt;code&gt;gpt-4o-mini&lt;/code&gt;. The worker is bound to tools. The evaluator is bound to the schema above with &lt;code&gt;.with_structured_output&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The routing after evaluation is where the loop's whole personality lives:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def route_based_on_evaluation(self, state: State) -&amp;gt; str:
    if state[&quot;success_criteria_met&quot;] or state[&quot;user_input_needed&quot;]:
        return &quot;END&quot;
    if state.get(&quot;turn_count&quot;, 0) &amp;gt;= self.MAX_TURNS:
        return &quot;END&quot;
    return &quot;worker&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three ways out. The criteria is met. The assistant is stuck and needs the user. Or the loop has spent its five turns. Otherwise it loops back to the worker, and this time the worker gets the feedback baked into its system prompt:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if state.get(&quot;feedback_on_work&quot;):
    system_message += f&quot;&quot;&quot;
Previously you thought you completed the assignment, but your reply was rejected because the success criteria was not met.
Feedback:
{state[&quot;feedback_on_work&quot;]}
Please continue the assignment...&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That feedback injection is what makes the retry worth anything. The worker does not just try again blind. It tries again knowing why the last attempt was rejected.&lt;/p&gt;
&lt;p&gt;One design detail I care about: the evaluator is told not to confuse &quot;not done yet&quot; with &quot;stuck.&quot; A failed criteria means retry, not stop.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;For user_input_needed: set True ONLY if the assistant explicitly asked the user a question or stated it cannot proceed.
Do NOT set user_input_needed=True just because the criteria was not met — the assistant should simply retry.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without that line, the evaluator bails to the user the first time the worker misses, and the retry loop never actually retries.&lt;/p&gt;
&lt;h3 id=&quot;what-broke-and-what-i-changed&quot;&gt;What broke, and what I changed&lt;/h3&gt;
&lt;p&gt;The loop is stateless by design. Wingman reconstructs the full conversation from DynamoDB on every request and runs a fresh graph invocation with a throwaway thread ID, so there is no in-memory state to lose between Lambda cold starts. That part held up.&lt;/p&gt;
&lt;p&gt;The timeout did not.&lt;/p&gt;
&lt;p&gt;A worker-evaluator loop is slow. Five turns, each turn a worker call plus tool calls plus an evaluator call, adds up to real wall-clock time. I set the Lambda timeout to 300 seconds to give the loop room:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;# LangGraph agent loops can take a while.
timeout     = 300  # 5 minutes (Lambda max is 15 min)
memory_size = 1024
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That number is a lie if the request comes through API Gateway. The HTTP API has a hard 29-second integration cap that Lambda's 300 seconds cannot override. So a long agent run would keep working inside Lambda while the gateway had already given up on the caller. The function billed for the full run. The user got nothing.&lt;/p&gt;
&lt;p&gt;The fix was to stop routing long tasks through the gateway at all. I added a Lambda Function URL, which is a direct HTTPS endpoint with no 29-second ceiling:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;# Lambda function URL — direct HTTPS invoke, bypasses API Gateway 29s timeout.
resource &quot;aws_lambda_function_url&quot; &quot;backend&quot; {
  function_name      = aws_lambda_function.backend.function_name
  authorization_type = &quot;NONE&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the gateway stays for the fast endpoints, and the agent loop can run against the Function URL without a proxy in the middle deciding it took too long.&lt;/p&gt;
&lt;p&gt;There is a second sharp edge I have not fully closed. &lt;code&gt;turn_count&lt;/code&gt; only increments inside the evaluator node:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&quot;turn_count&quot;: state.get(&quot;turn_count&quot;, 0) + 1,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the worker can loop &lt;code&gt;worker -&amp;gt; tools -&amp;gt; worker -&amp;gt; tools&lt;/code&gt; several times before it ever hands off to the evaluator, and none of those hops count against &lt;code&gt;MAX_TURNS&lt;/code&gt;. So the five-turn cap is really &quot;five evaluator turns,&quot; not &quot;five model calls.&quot; A task that keeps calling tools without producing a final answer can burn far more time than the cap implies. The honest fix is to count worker iterations too, or add a wall-clock budget inside the run, rather than trusting the evaluator counter to bound cost. That one is still on my list.&lt;/p&gt;
&lt;h3 id=&quot;takeaway-1&quot;&gt;Takeaway&lt;/h3&gt;
&lt;p&gt;An evaluator that gates the worker's output is the cheap half of a self-correcting agent. The expensive half is making the retry ceiling account for every model call, not just the ones you happened to count.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22871/building-a-worker-evaluator-retry-loop-in-langgraph-and-where-it-bites-you-on-lambda</guid>
<pubDate>Fri, 17 Jul 2026 05:44:06 +0000</pubDate>
</item>
<item>
<title>Memory in LLM Agents, Explained: From Stateless Calls to Long-Term Memory</title>
<link>https://coderlegion.com/21074/memory-in-llm-agents-explained-from-stateless-calls-to-long-term-memory</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Most explanations of agent memory start at the wrong altitude. They jump straight to &quot;use a vector database&quot; without explaining what problem that solves, or why a vector database is sometimes the wrong tool entirely.&lt;/p&gt;
&lt;p&gt;This post walks through memory the way I built it: as a progression of seven small, runnable scripts, starting from a completely stateless LLM call and ending at a two-layer system with separate short-term and long-term memory. Each step introduces exactly one new concept. By the end, the line between short-term and long-term memory stops being fuzzy and becomes a concrete architectural decision: what is scoped to a thread, and what is scoped to a user.&lt;/p&gt;
&lt;p&gt;All code referenced here is in the open-source repo at the end of this post. Every script is self-contained and runnable on its own.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-stateless-the-baseline-problem&quot;&gt;1. Stateless: the baseline problem&lt;/h3&gt;
&lt;p&gt;The simplest possible setup is a single call to an LLM with no history attached. Tell it your name, ask it your name in the next message, and it has no idea. Every call starts from zero.&lt;/p&gt;
&lt;p&gt;This is not a bug. It is the literal absence of memory, and it is the baseline every other pattern in this guide is solving for.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-short-term-memory-in-ram&quot;&gt;2. Short-term memory in RAM&lt;/h3&gt;
&lt;p&gt;The most direct fix: keep the full conversation in a Python list, and send that list with every call to the model.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;history.append({&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_input})
response = llm.invoke(history)
history.append({&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: response.content})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the model can recall earlier turns, because it is literally being shown them every time. The catch: this history lives in process memory. Restart the script and it is gone. This pattern is memory in the loosest sense, useful for a single uninterrupted session, useless across sessions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-persistence-and-threads&quot;&gt;3. Persistence and threads&lt;/h3&gt;
&lt;p&gt;The next step is durability. Write the conversation history to SQLite after every turn, and reload it on startup.&lt;/p&gt;
&lt;p&gt;This alone solves the restart problem. The second piece is the thread_id: rather than one global history, each thread_id is its own isolated conversation. Multiple conversations can live in the same store without bleeding into each other, and you can switch between them on demand.&lt;/p&gt;
&lt;p&gt;At this point memory has gone from &quot;exists only while the process is running&quot; to &quot;exists independently of the process, scoped to a conversation.&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-sliding-window&quot;&gt;4. Sliding window&lt;/h3&gt;
&lt;p&gt;A real conversation grows without bound, and sending the full history on every call gets expensive fast, both in tokens and in latency. The sliding window pattern fixes the cost problem directly: a &lt;code&gt;@before_model&lt;/code&gt; middleware hook trims the stored history down to the last N messages right before every call to the model.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@before_model
def trim_to_window(messages):
    return messages[-WINDOW:]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Token cost is now bounded, regardless of how long the conversation runs. The tradeoff is blunt: anything said more than N turns ago is permanently inaccessible to the model. If the user mentioned an important constraint 10 turns ago and the window is 4, that constraint is gone.&lt;/p&gt;
&lt;p&gt;This is the right tool when recent context is what matters and the conversation is expected to be short or self-contained.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-summarization&quot;&gt;5. Summarization&lt;/h3&gt;
&lt;p&gt;Summarization is the more graceful version of the same idea. Instead of discarding old messages outright, a &lt;code&gt;@before_model&lt;/code&gt; middleware hook compresses them into a short summary once the stored message count crosses a threshold, while keeping the most recent few messages verbatim alongside that summary.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@before_model
def summarize_if_needed(messages):
    if len(messages) &amp;gt; SUMMARIZE_AFTER:
        old, recent = messages[:-KEEP_RECENT], messages[-KEEP_RECENT:]
        summary = llm.invoke([SUMMARY_PROMPT, *old])
        return [summary_message(summary), *recent]
    return messages
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This retains the gist of everything that happened earlier in the conversation, at the cost of losing fine-grained detail. A sliding window would have kept the literal text of the last N turns and nothing before that; summarization keeps a compressed signal of everything before that, plus the literal text of the most recent few turns.&lt;/p&gt;
&lt;p&gt;Neither pattern is strictly better. Sliding window is cheaper and simpler. Summarization preserves more of the conversation's shape, at the cost of an extra LLM call to generate the summary.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-long-term-memory-the-user-profile&quot;&gt;6. Long-term memory: the user profile&lt;/h3&gt;
&lt;p&gt;Every pattern so far is still short-term memory. It lives within one conversation thread and, even with persistence, none of it is shared if the user starts a different thread.&lt;/p&gt;
&lt;p&gt;Long-term memory is a different scope entirely: durable facts about a user that should be available no matter which conversation they are currently having. The pattern here uses a second LLM call after every turn to extract durable facts (name, job, stated preferences) and write them to a key-value store (LangGraph's &lt;code&gt;SqliteStore&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;facts = extract_facts_llm.invoke([extraction_prompt, *recent_turns])
store.put(namespace=(&quot;user&quot;, user_id), key=&quot;profile&quot;, value=facts)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the next session, even a completely separate thread, those facts are loaded from the store and injected into the system prompt. This is the first pattern in the series where memory survives across sessions that have no shared conversation history at all.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-combining-short-term-and-long-term-memory&quot;&gt;7. Combining short-term and long-term memory&lt;/h3&gt;
&lt;p&gt;The full architecture uses both layers together, each with a different scope:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Short-term memory&lt;/strong&gt;, via &lt;code&gt;SqliteSaver&lt;/code&gt;: conversation history, scoped to a &lt;code&gt;thread_id&lt;/code&gt;. Restored automatically on reconnect to the same thread.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long-term memory&lt;/strong&gt;, via &lt;code&gt;SqliteStore&lt;/code&gt;: durable behavioral rules, scoped to a &lt;code&gt;user_id&lt;/code&gt;. The agent saves rules autonomously via a &lt;code&gt;save_rule&lt;/code&gt; tool, and those rules apply to every future session for that user, regardless of thread.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The clarifying detail: open a new thread for the same user, and the conversation history resets, but the rules that user taught the agent in a previous thread are already active. Switch to a different user entirely, and that user inherits none of the first user's rules. Each user's long-term memory is fully isolated.&lt;/p&gt;
&lt;p&gt;This is the insight that took the fuzziness out of &quot;short-term vs long-term memory&quot; for me. They are not the same kind of memory at different durations. They are scoped along different dimensions: STM resets per thread, LTM persists per user across every thread that user ever opens.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;choosing-a-pattern&quot;&gt;Choosing a pattern&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Situation &lt;/th&gt;&lt;th&gt;Pattern&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Single-session prototype, no persistence needed &lt;/td&gt;&lt;td&gt;STM in RAM&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Conversation needs to survive restarts &lt;/td&gt;&lt;td&gt;STM plus SQLite with threads&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Long conversations, recent context matters most, cost-sensitive &lt;/td&gt;&lt;td&gt;Sliding window&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Long conversations, full context shape matters &lt;/td&gt;&lt;td&gt;Summarization&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Facts about the user should persist across sessions &lt;/td&gt;&lt;td&gt;Long-term memory, user profile&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Production agent with both ongoing conversations and persistent user behavior &lt;/td&gt;&lt;td&gt;STM and LTM combined&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;further-reading&quot;&gt;Further reading&lt;/h3&gt;
&lt;p&gt;This series draws directly on LangChain and LangGraph's memory primitives. For the underlying API reference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.langchain.com/oss/python/langchain/short-term-memory&quot;&gt;LangChain short-term memory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.langchain.com/oss/python/langchain/short-term-memory#before-model&quot;&gt;Before-model middleware&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.langchain.com/oss/python/langchain/middleware/overview&quot;&gt;Middleware overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.langchain.com/oss/python/langchain/short-term-memory#summarize-messages&quot;&gt;Message summarization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.langchain.com/oss/python/langchain/long-term-memory&quot;&gt;LangChain long-term memory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.langchain.com/oss/python/langgraph/stores&quot;&gt;LangGraph stores&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Memory in LLM applications is not one feature, it is a set of independent tradeoffs: durability versus simplicity, recall versus token cost, thread-scoped versus user-scoped. Building each pattern in isolation, in order, makes those tradeoffs visible rather than hidden behind a single &quot;add memory&quot; abstraction.&lt;/p&gt;
&lt;p&gt;Full source, all 7 scripts, self-contained and runnable: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/f2015537/llm-memory-patterns&quot;&gt;https://github.com/f2015537/llm-memory-patterns&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21074/memory-in-llm-agents-explained-from-stateless-calls-to-long-term-memory</guid>
<pubDate>Sun, 21 Jun 2026 08:23:59 +0000</pubDate>
</item>
<item>
<title>Skill-Based Sub-Agents with LangGraph4j and Spring AI</title>
<link>https://coderlegion.com/16271/skill-based-sub-agents-with-langgraph4j-and-spring-ai</link>
<description>&lt;h3 id=&quot;combine-tools-amp-skill-to-create-agent&quot;&gt;Combine Tools &amp;amp; Skill to create Agent&lt;/h3&gt;
&lt;p&gt;Modern agentic systems usually expose two strong but separate abstractions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tools&lt;/strong&gt;, which give an LLM executable capabilities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt;, which package reusable instructions for a narrow task&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This article shows how to combine both ideas in [LangGraph4j] so that a skill is not just prompt text, but a fully operational &lt;strong&gt;sub-agent exposed as a tool&lt;/strong&gt;. The result is a practical pattern for building modular multi-agent systems in Java with &lt;strong&gt;[Spring AI]&lt;/strong&gt; and &lt;strong&gt;[LangGraph4j]&lt;/strong&gt;, while keeping each agent focused, reusable, and low in context cost.&lt;/p&gt;
&lt;p&gt;Starting from a standard ReACT agent, the approach implemented is quite simple.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Load a &lt;code&gt;SKILL.md&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Read its front matter to define the tool contract ( name and argument description)&lt;/li&gt;
&lt;li&gt;Use the skill body as the sub-agent system prompt&lt;/li&gt;
&lt;li&gt;Restrict the sub-agent to only the tools declared by the skill&lt;/li&gt;
&lt;li&gt;Register the sub-agent in a parent ReAct agent as if it were a normal tool&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In other words, a skill becomes an &lt;em&gt;executable tool-backed agent&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id=&quot;why-this-pattern-matters&quot;&gt;Why This Pattern Matters&lt;/h3&gt;
&lt;p&gt;The standard ReAct pattern already gives an LLM a strong execution model: reason, choose a tool, inspect the result, and continue until the task is complete. That model is useful because the LLM does not need to commit to a fixed plan up front. It can adapt after every tool call.&lt;/p&gt;
&lt;p&gt;The next step is to let a tool itself be implemented by another agent.&lt;/p&gt;
&lt;p&gt;Once that happens, the parent agent is no longer forced to know every operational detail. It only needs to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;which specialized capability exists&lt;/li&gt;
&lt;li&gt;when to invoke it&lt;/li&gt;
&lt;li&gt;what input to pass&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The specialized logic is delegated to a narrower agent with its own instructions and its own restricted toolset. This gives us a clean &lt;strong&gt;sub-agent architecture&lt;/strong&gt; without inventing a new orchestration primitive. We reuse the same tool-calling semantics that ReAct agents already understand well.&lt;/p&gt;
&amp;lt;!--
## Advantages

There are multiple advantages to use this approach:

### Reuse Tool execution plan (coordination)

One of the most important feature of the LLM, since from beginnig, was able to define a tool execution planning to resolve the problem. The ReAct agent was introdced to add an execution layer and an evaluation loop to execute tools and involve again LLM,  based on tool execution result in order to dynamically revaluate the execution planning itself.
If we consider that a tool can involve a ReACT agent in turn we gain the sub agent involvment that colud involve other sub agents in turn in a iterative way

### Minimize agent context (less token)

Declaring a sub agent as a tool, when its parent agent plan to call it, it provides the information requested by sub agent extracting them from the current context automatically reducing them for the specific call. This guarantee that the sub agent receives only the required information that become the user input for it.
Moreover for each sub agents we can declare the `allowed-tools` that further leaning the context making the agent more efficient and effective
--&amp;gt;
&lt;h3 id=&quot;why-reusing-tool-semantics-is-powerful&quot;&gt;Why Reusing Tool Semantics Is Powerful&lt;/h3&gt;
&lt;p&gt;One of the most valuable properties of LLM-based agents is their ability to build and revise a tool execution plan while reasoning over intermediate results. ReAct made this practical by combining thought, action, and observation in a loop.&lt;/p&gt;
&lt;p&gt;When a tool call can transparently invoke another agent, we preserve that planning model instead of replacing it.&lt;/p&gt;
&lt;p&gt;The parent agent can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;decide that a specialized capability is needed&lt;/li&gt;
&lt;li&gt;provide only the task-relevant context&lt;/li&gt;
&lt;li&gt;wait for a result exactly as it would for a standard tool&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The child agent can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;run its own reasoning loop&lt;/li&gt;
&lt;li&gt;invoke its own allowed tools&lt;/li&gt;
&lt;li&gt;return a compact outcome to the parent&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives hierarchical coordination without exposing the full inner workflow to the parent prompt.&lt;/p&gt;
&lt;h3 id=&quot;minimizing-context-and-token-usage&quot;&gt;Minimizing Context and Token Usage&lt;/h3&gt;
&lt;p&gt;This pattern also improves context discipline.&lt;/p&gt;
&lt;p&gt;Without sub-agents, a single generalist agent often needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;every tool definition&lt;/li&gt;
&lt;li&gt;all operating instructions for every domain&lt;/li&gt;
&lt;li&gt;every procedural rule in one large system prompt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That quickly becomes expensive and unreliable.&lt;/p&gt;
&lt;p&gt;With skill-based sub-agents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the parent only sees high-level capabilities&lt;/li&gt;
&lt;li&gt;each child sees only the instructions relevant to its task&lt;/li&gt;
&lt;li&gt;each child receives only the tools it is allowed to use&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This narrower context has practical benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fewer tokens sent to the model&lt;/li&gt;
&lt;li&gt;lower prompt interference between unrelated domains&lt;/li&gt;
&lt;li&gt;clearer tool choice&lt;/li&gt;
&lt;li&gt;better modularity as the system grows&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;The &lt;code&gt;allowed-tools&lt;/code&gt; skill property plays an important role here anabling tool filtering. It turns tool access into an explicit design decision instead of an accidental side effect of what happened to be registered globally.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;reference-implementation&quot;&gt;Reference implementation&lt;/h3&gt;
&lt;p&gt;I've started implementation in the new [LangGraph4j] release stream &lt;code&gt;1.9&lt;/code&gt; currently in active development. The implementation is centered on [`SkilledReactSubAgent`], which turns a markdown skill into both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a compiled LangGraph4j sub-graph&lt;/li&gt;
&lt;li&gt;a Spring AI tool (i.e. &lt;code&gt;ToolCallback&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That means the same component can participate in the graph as an agent and present itself to the parent LLM as a callable tool.&lt;/p&gt;
&lt;p&gt;In particular the flow is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Parse the &lt;code&gt;SKILL.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Extract &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; from front matter&lt;/li&gt;
&lt;li&gt;Optionally filter tools using &lt;code&gt;allowed-tools&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use the markdown body as the sub-agent instructions&lt;/li&gt;
&lt;li&gt;Compile the agent&lt;/li&gt;
&lt;li&gt;Wrap it into a function tool whose input is a single &lt;code&gt;context&lt;/code&gt; field&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;The important design choice is that the &lt;strong&gt;tool definition is derived from the skill metadata&lt;/strong&gt;, while the &lt;strong&gt;agent behavior is derived from the skill content&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;skill-format&quot;&gt;Skill Format&lt;/h4&gt;
&lt;p&gt;The supported skill format is &lt;/p&gt;
&lt;h3 id=&quot;section&quot;&gt;&lt;/h3&gt;
&lt;p&gt;name: &lt;br&gt;
description: |&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;multi line description&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;allowed-tools-&quot;&gt;allowed-tools: &lt;/h3&gt;




### From Skill to Tool-Backed Agent

The conversion happens in `SkilledReactSubAgent.Builder#build(...)`. This is the crucial bridge where:

* the skill metadata defines the external API
* the skill body defines the internal operating behavior
* the allowed tools define the sub-agent execution boundary

The resulting agent is then wrapped as a Spring AI function tool.

&amp;gt; From the parent agent point of view, this is just another tool. Internally, it is a complete ReAct-capable agent workflow.


## Examples

### Purchase Assistant Agent

This is an agent that allow to select a produdct from a reference marketplace and purchase it. Below the simplified component diagram.

![](https://bsorrentino.github.io/bsorrentino/assets/langgraph-java/sub-agents/PurchaseAssistantAgent1.png)

### Marketplace Skill

&lt;pre&gt;
---
name: agent-marketplace
description: |
    marketplace agent, ask for information about products. This is agent that provides the information on the product marketplace.
    required data are:
    * product name
allowed-tools: searchByProduct
---

## Retrieve product information
We need all information about about the product of interest in particular is mandatory to have
* product name

with such information we have to invoke tool `searchByProduct` and return the result
&lt;/pre&gt;

### Payment Skill

&lt;pre&gt;
---
name: agent-payment
description: |
    payment agent, this is the agent that provides payment service.
    required data are:
    * Product name
    * Product price
    * Product currency
    * the International Bank Account Number (IBAN) - optional

allowed-tools:
    - submit-payment
    - retrieve-iban
---

## submit a payment for purchasing a specific product
We need all information about purchasing to allow the payment

* Product name
* Product price
* Product price currency
* International Bank Account Number (IBAN)

With such information you must invoke tool `submit-payment` and return the result of the payment transaction plus the IBAN.
If IBAN is not provided invoke tool `retrieve-iban` to retrieve the required IBAN.

&lt;/pre&gt;

Observing the skills above they define three different concerns:

* `name`: the tool name exposed to the parent agent
* `description`: the tool description shown to the model
* `allowed-tools`: the only tools available inside the sub-agent

Everything after the front matter becomes the sub-agent system prompt.

&amp;gt; Again, this is a strong separation of concerns. The parent agent sees a concise tool contract, while the child agent receives richer operating instructions.

## Finally the code

Below a representative code snippet that show how wires two skill-based sub-agents into a parent agent:

```java
var subAgentMarketplace = SkilledReactSubAgent.builder()
        .chatModel(chatModel)
        .tools(Marketplace.tools())
        .build(compileConfig,
                SkillSource.of(Paths.get(&quot;skills/agent-marketplace/&quot;)));

var subAgentPayment = SkilledReactSubAgent.builder()
        .chatModel(chatModel)
        .tools(Payment.tools())
        .build(compileConfig,
                SkillSource.of(Paths.get(&quot;skills/agent-payment/&quot;)));

var purchaseAssistantAgent = AgentExecutorEx.builder()
        .chatModel(chatModel)
        .tool(subAgentMarketplace)
        .tool(subAgentPayment)
        .build(compileConfig);

var input = &quot;&quot;&quot;
        search for product 'X'.
        If found proceed to payment with IBAN US82WEST1234567890123456 
        to purchase it
        &quot;&quot;&quot;;

var result = purchaseAssistantAgent.invoke( 
        GraphInput.args( Map.of(&quot;messages&quot;, new UserMessage(input) )), runnableConfig); 

```

This composition is interesting because the purchase agent (the parent) does not need detailed knowledge of:

* how product lookup works
* how payment submission works
* when IBAN retrieval is required

Those rules are delegated to the sub agents themselves.

That is a simple but good example of behavioral encapsulation. The parent agent only decides to involve marketplace agent to retrieve product and after that to involve payment agent to purchase.

This is a very basic but representative example, it is useful because it shows the intended layering:

* the user gives a single business request
* the parent agent decomposes the work
* specialized sub-agents execute their own logic
* the final answer comes back through the normal tool-response path

&amp;gt; Interesting note that each sub agent, potentially, can relies on different LLM chat model so we can further optimize overall agent execution process

Below a more detailed componet diagram generated by [LangGraph4j] itself.

![](https://bsorrentino.github.io/bsorrentino/assets/langgraph-java/sub-agents/PurchaseAssistantAgent.png)

From this diagram it is quite clear that the sub agents are nothing but sub-graphs

## Design Advantages

This approach has several practical advantages.

1. **Reuse Existing Agent Patterns**
   
   There is no need to invent a separate orchestration protocol for sub-agents. A sub-agent is exposed as a tool, so the parent agent can use standard ReAct behavior without extra prompting complexity.

2. **Better Modularity**

   Each skill owns:
   * its tool-facing identity
   * its internal instructions
   * its allowed tool boundary

   That makes the system easier to evolve than a single monolithic agent prompt.

3. **Smaller and Cleaner Context**

   The child agent receives only what it needs. This reduces token usage and lowers the risk that unrelated instructions distort tool choice or task execution.

4. **Safer Capability Scoping**

   By filtering tools from `allowed-tools`, the framework prevents a child agent from accidentally seeing tools outside its intended domain.

5. **Better Reusability**

   A skill can be packaged, versioned, and reused across different parent agents. The same marketplace or payment skill can participate in many workflows as long as the tool contract remains stable.

## Tradeoffs and Implementation Considerations

This pattern is strong, but it is not free of tradeoffs.

1. **The Parent Must Summarize Well**

   Because the child receives a single `context` field, the parent agent needs to pass the relevant facts clearly. If critical information is omitted, the child may need extra recovery steps or may fail to act.

2. **Tool Descriptions Matter More**

   Since the parent sees the child as a tool, the `name` and `description` fields in the skill front matter become part of the routing surface for the LLM. Weak metadata will reduce invocation quality.

3. **Prompt Boundaries Need Discipline**

   The skill body is effectively the child system prompt. If it is vague, contradictory, or overloaded, the benefits of modularity disappear. Skills need to stay focused.

4. **Observability Is Important**

   Nested agents can make execution harder to inspect if tracing is weak. LangGraph4j's graph structure and step streaming help here, but production systems should still invest in logging and traceability.

## Why LangGraph4j Fits This Well

[LangGraph4j] is a good fit for this pattern because it already models agent workflows as explicit graphs and integrates naturally with Spring AI tool abstractions.

That matters for two reasons:

* the sub-agent is not just prompt composition; it is a compiled graph with state and execution hooks
* the parent integration remains ergonomic because Spring AI already treats capabilities as tools
* [LangGraph4j] support interruptions in subgraph enabling **Human In The Loop** also when involving sub agents

So the implementation is not forcing two incompatible ideas together. It is aligning:

* LangGraph4j graph execution
* Spring AI tool calling
* markdown-defined skills

into a single reusable agent-building pattern.

## Conclusion

Turning a skill into a sub-agent is a small change with a large impact.

It lets us:

* preserve the strengths of the ReAct loop
* delegate specialized behavior cleanly
* reduce context size
* constrain tool access per domain
* build multi-agent systems out of reusable skill modules

The idea is implemented in a compact and pragmatic way: a `SKILL.md` file becomes the source of both the sub-agent prompt and the tool contract exposed to the parent agent.

For Java teams building agentic applications with Spring AI and LangGraph4j, this is a practical pattern for moving from a single large agent toward a modular multi-agent design without abandoning the tool semantics that current LLMs already handle well.

The implementation is in progress on release `1.9-SNAPSHOT` in develop branch of [LangGraph4j] repository but the first result are very promising.

Hope this could help and encourage usage of [LangGraph4j] for your next Agentic Workflow. Checkout project, try it and let me know your feedback and... happy AI coding! 


[LangGraph4j]: https://github.com/langgraph4j/langgraph4j
[Spring AI]: https://spring.io/projects/spring-ai

</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16271/skill-based-sub-agents-with-langgraph4j-and-spring-ai</guid>
<pubDate>Thu, 30 Apr 2026 21:42:33 +0000</pubDate>
</item>
<item>
<title>LangGraph4j</title>
<link>https://coderlegion.com/11213/langgraph4j</link>
<description>&lt;h3 id=&quot;why-langgraph4j&quot;&gt;Why LangGraph4j&lt;/h3&gt;
&lt;p&gt;A while ago, when I started working in AI, I chose &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/langchain-ai&quot;&gt;LangChain&lt;/a&gt; to build LLM-powered apps because it made introduce LLM into applications practical and effective. Then, in January 2024, &lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.langchain.dev/langgraph/&quot;&gt;LangChain announced LangGraph&lt;/a&gt;: a module built on top of LangChain that enables cyclical graphs for real-world agent runtimes. That was the missing piece for building agents in a controlled and safe way.&lt;/p&gt;
&lt;p&gt;I work with teams whose IT stack is mainly Java, and they want to experiment with LLMs in production-grade systems. That’s why I built &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/bsorrentino/langgraph4j&quot;&gt;LangGraph4j&lt;/a&gt;&lt;/strong&gt;: to bring the LangGraph approach to the Java ecosystem, leverage my experience with LangChain and LangGraph, and deliver on a simple promise: &quot;&lt;strong&gt;learn once, apply everywhere&lt;/strong&gt;&quot;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LangGraph4j&lt;/strong&gt; is inspired by [LangGraph] and designed to help Java developers build sophisticated AI-powered agents and applications. It integrates cleanly with established Java LLM frameworks like &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/langchain4j&quot;&gt;LangChain4j&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://spring.io/projects/spring-ai&quot;&gt;Spring AI&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;what-you-get&quot;&gt;What you get&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Controlled execution&lt;/strong&gt; with explicit graph paths and safe loops&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parity with LangGraph concepts&lt;/strong&gt;, without leaving the Java ecosystem&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drop-in integration&lt;/strong&gt; with Java LLM frameworks and existing codebases&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h3&gt;
&lt;p&gt;LangGraph4j allows you to build &lt;strong&gt;stateful, multi-agent applications&lt;/strong&gt; by defining cyclical graphs where different components (agents, tools, or custom logic) interact with a shared state. &lt;/p&gt;
&lt;h4 id=&quot;-key-features&quot;&gt;⭐ &lt;strong&gt;Key Features&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Core Graph Model&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stateful Execution&lt;/strong&gt; - Manage shared state across graph nodes for memory and context awareness&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cyclical Graphs&lt;/strong&gt; - Support for loops needed in real agent-based architectures&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Explicit Control Flow&lt;/strong&gt; - Clear, definable paths and transitions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Reliability &amp;amp; Long-Running Workflows&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Checkpoints&lt;/strong&gt; - Save and replay graph state at any point, enabling long-running workflows and Human-in-the-Loop (HITL)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Integration &amp;amp; Modularity&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modularity&lt;/strong&gt; - Build complex systems from reusable graphs (for example, a SubGraph)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility&lt;/strong&gt; - Integrate with various LLM providers and custom Java logic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Observability&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Observability &amp;amp; Debugging&lt;/strong&gt; - Use hooks as interception points around node and edge execution to add logging, metrics, or traces without contaminating business logic&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-to-get-involved&quot;&gt;&lt;strong&gt;How to Get Involved&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Start here&lt;/strong&gt;: try the Quickstart in the docs and run your first graph in minutes.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Documentation&lt;/strong&gt;: Visit the &lt;a rel=&quot;nofollow&quot; href=&quot;https://langgraph4j.github.io/langgraph4j/&quot;&gt;official documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DeepWiki&lt;/strong&gt;: Check out the community docs on &lt;a rel=&quot;nofollow&quot; href=&quot;https://deepwiki.com/langgraph4j/langgraph4j&quot;&gt;DeepWiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latest Releases&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/langgraph4j/langgraph4j/releases&quot;&gt;Check the GitHub releases page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Requirements&lt;/strong&gt;: Minimum Java 17 supported&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Community&lt;/strong&gt;: Join the &lt;a rel=&quot;nofollow&quot; href=&quot;https://discord.gg/szVVztSYKh&quot;&gt;Discord community&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Github Repository&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/langgraph4j/langgraph4j&quot;&gt;langgraph4j/langgraph4j&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maven Central&lt;/strong&gt;: Available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://central.sonatype.com/&quot;&gt;Maven Central&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;License&lt;/strong&gt;: MIT - Open for contributions&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;f you're intrigued by this project and want to support me in putting effort into this development journey, checkout the project, try it out, and let me know your feedback. Don't forget to star it ⭐️!&lt;br&gt;
In the meantime, happy AI coding &lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/11213/langgraph4j</guid>
<pubDate>Sat, 07 Feb 2026 15:07:31 +0000</pubDate>
</item>
<item>
<title>LangChain vs LangGraph vs LangSmith: Understanding the Ecosystem</title>
<link>https://coderlegion.com/9900/langchain-vs-langgraph-vs-langsmith-understanding-the-ecosystem</link>
<description>&lt;blockquote&gt;&lt;p&gt;Building LLM apps isn’t just about prompts anymore.&lt;br&gt;
It’s about &lt;strong&gt;composition&lt;/strong&gt;, &lt;strong&gt;orchestration&lt;/strong&gt;, and &lt;strong&gt;observability&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;tl-dr&quot;&gt;TL;DR&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LangChain&lt;/strong&gt; provides the foundational building blocks for creating LLM applications through modular components and a unified interface for working with different AI providers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LangGraph&lt;/strong&gt; extends this foundation with &lt;strong&gt;stateful, graph-based orchestration&lt;/strong&gt; for complex multi-agent workflows requiring loops, branching, and persistent state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LangSmith&lt;/strong&gt; completes the picture by offering &lt;strong&gt;observability, tracing, and evaluation&lt;/strong&gt; tools for debugging and monitoring LLM applications in production.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Use:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LangChain&lt;/strong&gt; for straightforward chains and RAG systems&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LangGraph&lt;/strong&gt; when you need sophisticated state management and agent coordination&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LangSmith&lt;/strong&gt; throughout development and production for visibility into behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;hands-on-github-repositories&quot;&gt;Hands-on GitHub Repositories&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LangChain RAG Project&lt;/strong&gt; → &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rajkundalia/langchain-rag-project&quot;&gt;https://github.com/rajkundalia/langchain-rag-project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LangGraph Analyzer&lt;/strong&gt; → &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rajkundalia/langgraph-analyzer&quot;&gt;https://github.com/rajkundalia/langgraph-analyzer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LangSmith Learning&lt;/strong&gt; → &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rajkundalia/langsmith-learning&quot;&gt;https://github.com/rajkundalia/langsmith-learning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;introduction-1&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;The landscape of LLM application development has evolved rapidly since 2022.&lt;/p&gt;
&lt;p&gt;What began as simple prompt–response interactions has grown into &lt;strong&gt;multi-step workflows&lt;/strong&gt; involving retrieval systems, tool usage, autonomous agents, and long-running processes. This evolution introduced &lt;strong&gt;new problems at each stage&lt;/strong&gt; of the development lifecycle.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The composition problem&lt;/strong&gt; → How do you connect prompts, models, tools, and data?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The orchestration problem&lt;/strong&gt; → How do you manage branching, retries, loops, and shared state?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The observability problem&lt;/strong&gt; → How do you debug, evaluate, and monitor these systems?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The LangChain ecosystem emerged to address each layer:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Problem       &lt;/th&gt;&lt;th&gt;Tool      &lt;/th&gt;&lt;th&gt;Year&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Composition   &lt;/td&gt;&lt;td&gt;LangChain &lt;/td&gt;&lt;td&gt;2022&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Orchestration &lt;/td&gt;&lt;td&gt;LangGraph &lt;/td&gt;&lt;td&gt;2024&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Observability &lt;/td&gt;&lt;td&gt;LangSmith &lt;/td&gt;&lt;td&gt;2023–2024&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each tool targets a &lt;strong&gt;specific layer&lt;/strong&gt; in the LLM application stack.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;langchain-the-foundation&quot;&gt;LangChain: The Foundation&lt;/h3&gt;
&lt;p&gt;LangChain is the &lt;strong&gt;core framework&lt;/strong&gt; for building LLM-powered applications.&lt;/p&gt;
&lt;p&gt;Its primary goal is abstraction: different LLM providers expose different APIs, capabilities, and quirks. LangChain hides these differences behind a &lt;strong&gt;unified interface&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;core-building-blocks&quot;&gt;Core Building Blocks&lt;/h4&gt;
&lt;p&gt;LangChain is composed of modular, swappable components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prompts&lt;/strong&gt; – Templates and structured inputs for models&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Models&lt;/strong&gt; – OpenAI, Anthropic, Google, or local LLMs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory&lt;/strong&gt; – Conversation history and contextual state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools&lt;/strong&gt; – Function calls to external systems&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retrievers&lt;/strong&gt; – Vector databases and RAG pipelines&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;lcel-langchain-expression-language&quot;&gt;LCEL: LangChain Expression Language&lt;/h4&gt;
&lt;p&gt;What ties everything together is &lt;strong&gt;LCEL&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;LCEL introduces a &lt;strong&gt;declarative, pipe-based syntax&lt;/strong&gt; for composing chains:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;prompt | model | output_parser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of writing imperative glue code, you describe &lt;strong&gt;data flow&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;why-lcel-matters&quot;&gt;Why LCEL Matters&lt;/h4&gt;
&lt;p&gt;LCEL enables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automatic async, streaming, and batch execution&lt;/li&gt;
&lt;li&gt;Built-in LangSmith tracing&lt;/li&gt;
&lt;li&gt;Parallel execution of independent steps&lt;/li&gt;
&lt;li&gt;A unified &lt;code&gt;Runnable&lt;/code&gt; interface (&lt;code&gt;invoke&lt;/code&gt;, &lt;code&gt;batch&lt;/code&gt;, &lt;code&gt;stream&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes chains &lt;strong&gt;faster&lt;/strong&gt;, &lt;strong&gt;cleaner&lt;/strong&gt;, and easier to reason about.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;multi-provider-support&quot;&gt;Multi-Provider Support&lt;/h4&gt;
&lt;p&gt;LangChain supports dozens of LLM providers and integrations.&lt;/p&gt;
&lt;p&gt;You can switch providers by changing &lt;strong&gt;one line of configuration&lt;/strong&gt;, enabling:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vendor independence&lt;/li&gt;
&lt;li&gt;A/B testing across models&lt;/li&gt;
&lt;li&gt;Cost and latency optimization&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;when-langchain-is-enough&quot;&gt;When LangChain Is Enough&lt;/h4&gt;
&lt;p&gt;Use LangChain when your workflow is primarily:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Input → Process → Output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typical use cases include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chatbots with memory&lt;/li&gt;
&lt;li&gt;RAG-based Q&amp;amp;A systems&lt;/li&gt;
&lt;li&gt;Natural language → SQL generation&lt;/li&gt;
&lt;li&gt;Linear tool pipelines&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your application doesn’t need complex branching or shared long-lived state, &lt;strong&gt;LangChain is the right tool&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;section-1&quot;&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7a2mw7h25kemz76pn20y.png&quot; alt=&quot;LangChain Component Flow&quot;&gt;&lt;/h3&gt;
&lt;h3 id=&quot;langgraph-stateful-agent-orchestration&quot;&gt;LangGraph: Stateful Agent Orchestration&lt;/h3&gt;
&lt;p&gt;LangGraph solves the &lt;strong&gt;orchestration problem&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;As soon as your application needs to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;make decisions,&lt;/li&gt;
&lt;li&gt;loop,&lt;/li&gt;
&lt;li&gt;retry,&lt;/li&gt;
&lt;li&gt;or coordinate multiple agents, linear chains start to break down.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;graph-based-architecture&quot;&gt;Graph-Based Architecture&lt;/h4&gt;
&lt;p&gt;LangGraph models your application as a &lt;strong&gt;directed graph&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nodes&lt;/strong&gt; → processing steps or agents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edges&lt;/strong&gt; → execution flow between nodes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This enables patterns that are hard or impossible with chains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Loops and retries&lt;/li&gt;
&lt;li&gt;Conditional branching&lt;/li&gt;
&lt;li&gt;Parallel execution&lt;/li&gt;
&lt;li&gt;Shared, persistent state&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;state-as-a-first-class-concept&quot;&gt;State as a First-Class Concept&lt;/h4&gt;
&lt;p&gt;Every LangGraph workflow operates on a &lt;strong&gt;shared state object&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nodes receive the current state&lt;/li&gt;
&lt;li&gt;They compute updates&lt;/li&gt;
&lt;li&gt;Updates are merged back into state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This allows multiple agents to collaborate naturally.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Research agent gathers sources&lt;/li&gt;
&lt;li&gt;Fact-checking agent validates claims&lt;/li&gt;
&lt;li&gt;Synthesis agent produces the final answer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All without complex message passing.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;conditional-routing&quot;&gt;Conditional Routing&lt;/h4&gt;
&lt;p&gt;LangGraph supports &lt;strong&gt;conditional edges&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A function decides which node runs next based on runtime state:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Route customer queries to specialist agents&lt;/li&gt;
&lt;li&gt;Loop back when required information is missing&lt;/li&gt;
&lt;li&gt;Retry until success conditions are met&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;persistence-amp-checkpointing&quot;&gt;Persistence &amp;amp; Checkpointing&lt;/h4&gt;
&lt;p&gt;LangGraph includes built-in &lt;strong&gt;checkpointing&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Persist state across restarts&lt;/li&gt;
&lt;li&gt;Resume long-running workflows&lt;/li&gt;
&lt;li&gt;Support human-in-the-loop pauses&lt;/li&gt;
&lt;li&gt;Enable time-travel debugging&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is critical for production-grade agent systems.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;visualization-support&quot;&gt;Visualization Support&lt;/h4&gt;
&lt;p&gt;LangGraph workflows are inspectable and exportable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mermaid diagrams for documentation&lt;/li&gt;
&lt;li&gt;PNG images for presentations&lt;/li&gt;
&lt;li&gt;ASCII graphs for terminal debugging&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes complex agent systems &lt;strong&gt;understandable and communicable&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;when-you-need-langgraph&quot;&gt;When You Need LangGraph&lt;/h4&gt;
&lt;p&gt;Choose LangGraph when you need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Explicit shared state&lt;/li&gt;
&lt;li&gt;Runtime decision-making&lt;/li&gt;
&lt;li&gt;Retry and failure recovery&lt;/li&gt;
&lt;li&gt;Multi-agent coordination&lt;/li&gt;
&lt;li&gt;Long-running workflows&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A classic example is an &lt;strong&gt;autonomous research agent&lt;/strong&gt; that iteratively searches, reads, verifies, and synthesizes information.&lt;/p&gt;
&lt;h3 id=&quot;section-2&quot;&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e6w9w6egyyj8r4c3ricl.png&quot; alt=&quot;LangGraph State Machine Example&quot;&gt;&lt;/h3&gt;
&lt;h3 id=&quot;langsmith-the-observability-layer&quot;&gt;LangSmith: The Observability Layer&lt;/h3&gt;
&lt;p&gt;LangSmith answers the question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“What is my LLM application actually doing?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It doesn’t build workflows — it &lt;strong&gt;illuminates them&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;tracing-everything&quot;&gt;Tracing Everything&lt;/h4&gt;
&lt;p&gt;LangSmith captures full execution traces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prompts and responses&lt;/li&gt;
&lt;li&gt;Token usage and latency&lt;/li&gt;
&lt;li&gt;Component call stacks&lt;/li&gt;
&lt;li&gt;Errors and retries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can drill down from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a full workflow run&lt;br&gt;
→ to a single LLM call.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes debugging &lt;em&gt;dramatically&lt;/em&gt; easier.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;evaluation-amp-regression-testing&quot;&gt;Evaluation &amp;amp; Regression Testing&lt;/h4&gt;
&lt;p&gt;LangSmith allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Create evaluation datasets&lt;/li&gt;
&lt;li&gt;Run structured tests&lt;/li&gt;
&lt;li&gt;Track quality metrics&lt;/li&gt;
&lt;li&gt;Compare prompts and models&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This enables &lt;strong&gt;regression testing&lt;/strong&gt; for LLM apps — a must-have for production systems.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;production-monitoring&quot;&gt;Production Monitoring&lt;/h4&gt;
&lt;p&gt;In production, LangSmith tracks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Response times&lt;/li&gt;
&lt;li&gt;Error rates&lt;/li&gt;
&lt;li&gt;Token and cost trends&lt;/li&gt;
&lt;li&gt;Usage by workflow or user&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alerts help you catch issues early and optimize costs.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;framework-agnostic&quot;&gt;Framework-Agnostic&lt;/h4&gt;
&lt;p&gt;While LangSmith integrates seamlessly with LangChain and LangGraph, it’s &lt;strong&gt;not limited to them&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You can instrument &lt;em&gt;any&lt;/em&gt; LLM application with LangSmith.&lt;/p&gt;
&lt;h3 id=&quot;section-3&quot;&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dx2l37b7n29anhmsrrhh.png&quot; alt=&quot;LangSmith Diagram&quot;&gt;&lt;/h3&gt;
&lt;h3 id=&quot;quick-comparison&quot;&gt;Quick Comparison&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Tool      &lt;/th&gt;&lt;th&gt;Solves        &lt;/th&gt;&lt;th&gt;Use When&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;LangChain &lt;/td&gt;&lt;td&gt;Composition   &lt;/td&gt;&lt;td&gt;Linear workflows, RAG, simple agents&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;LangGraph &lt;/td&gt;&lt;td&gt;Orchestration &lt;/td&gt;&lt;td&gt;Branching, loops, shared state, multi-agent&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;LangSmith &lt;/td&gt;&lt;td&gt;Observability &lt;/td&gt;&lt;td&gt;Debugging, evaluation, production monitoring&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;section-4&quot;&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yia3pj7oyxnurc75suxn.png&quot; alt=&quot;Decision Tree: Which Tool to Use?&quot;&gt;&lt;/h3&gt;
&lt;h3 id=&quot;the-broader-ecosystem&quot;&gt;The Broader Ecosystem&lt;/h3&gt;
&lt;h4 id=&quot;langflow&quot;&gt;LangFlow&lt;/h4&gt;
&lt;p&gt;LangFlow provides a &lt;strong&gt;visual, drag-and-drop&lt;/strong&gt; interface for building LangChain workflows.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Great for prototyping&lt;/li&gt;
&lt;li&gt;Helpful for non-technical collaboration&lt;/li&gt;
&lt;li&gt;Often exported to code for production&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;model-context-protocol-mcp-&quot;&gt;Model Context Protocol (MCP)&lt;/h4&gt;
&lt;p&gt;MCP (by Anthropic) standardizes &lt;strong&gt;tool and resource access&lt;/strong&gt; for LLMs.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Works at the tool/retriever layer&lt;/li&gt;
&lt;li&gt;Complements LangChain and LangGraph&lt;/li&gt;
&lt;li&gt;Reduces custom integration effort&lt;/li&gt;
&lt;li&gt;Framework-agnostic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MCP does &lt;strong&gt;not&lt;/strong&gt; replace orchestration tools — it enhances connectivity.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The LangChain ecosystem is &lt;strong&gt;layered, not competitive&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LangChain&lt;/strong&gt; builds the core logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LangGraph&lt;/strong&gt; manages complex workflows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LangSmith&lt;/strong&gt; makes everything observable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most serious LLM applications will use &lt;strong&gt;more than one&lt;/strong&gt; of these tools.&lt;/p&gt;
&lt;p&gt;Start simple, add complexity only when needed, and &lt;strong&gt;never ship without observability&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;further-reading-amp-resources&quot;&gt;Further Reading &amp;amp; Resources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.datacamp.com/tutorial/langchain-vs-langgraph-vs-langsmith-vs-langflow&quot;&gt;https://www.datacamp.com/tutorial/langchain-vs-langgraph-vs-langsmith-vs-langflow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.datacamp.com/tutorial/langgraph-tutorial&quot;&gt;https://www.datacamp.com/tutorial/langgraph-tutorial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.datacamp.com/tutorial/langgraph-agents&quot;&gt;https://www.datacamp.com/tutorial/langgraph-agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.techvoot.com/blog/langchain-vs-langgraph-vs-langflow-vs-langsmith-2025&quot;&gt;https://www.techvoot.com/blog/langchain-vs-langgraph-vs-langflow-vs-langsmith-2025&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Video&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=vJOGC8QJZJQ&quot;&gt;https://www.youtube.com/watch?v=vJOGC8QJZJQ&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Academy Finxter Series (Excellent Deep Dive)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://academy.finxter.com/langchain-langsmith-and-langgraph/&quot;&gt;https://academy.finxter.com/langchain-langsmith-and-langgraph/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://academy.finxter.com/langsmith-and-writing-tools/&quot;&gt;https://academy.finxter.com/langsmith-and-writing-tools/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://academy.finxter.com/langgraph/&quot;&gt;https://academy.finxter.com/langgraph/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://academy.finxter.com/multi-agent-teams-preparation/&quot;&gt;https://academy.finxter.com/multi-agent-teams-preparation/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://academy.finxter.com/setting-up-our-multi-agent-team/&quot;&gt;https://academy.finxter.com/setting-up-our-multi-agent-team/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://academy.finxter.com/web-research-and-asynchronous-tools/&quot;&gt;https://academy.finxter.com/web-research-and-asynchronous-tools/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/9900/langchain-vs-langgraph-vs-langsmith-understanding-the-ecosystem</guid>
<pubDate>Sat, 17 Jan 2026 13:36:52 +0000</pubDate>
</item>
<item>
<title>Creating a chatbot for SQLite Databases</title>
<link>https://coderlegion.com/810/creating-a-chatbot-for-sqlite-databases</link>
<description>&lt;p&gt;We all use chatbots like ChatGPT, Perplexity, and Claude. But wouldn’t it be cool if a chatbot could directly talk to your database? And keep your data secure at the same time? Let’s build one!&lt;/p&gt;
&lt;p&gt;Here’s how we can build it, step by step:&lt;/p&gt;
&lt;h3 id=&quot;technologies-we-ll-use-&quot;&gt;Technologies We’ll Use:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Frontend&lt;/li&gt;
&lt;li&gt;Next.js (React framework)&lt;/li&gt;
&lt;li&gt;Copilotkit (for interacting with sql agent)&lt;/li&gt;
&lt;li&gt;Backend&lt;/li&gt;
&lt;li&gt;Fastapi (Python backend framework)&lt;/li&gt;
&lt;li&gt;langgraph and langchain (for building sql agent)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We’ll create a &lt;strong&gt;SQL agent&lt;/strong&gt; using LangGraph. This agent will connect to your database and allow you to chat with it in a structured way.&lt;/p&gt;
&lt;p&gt;Once the agent is ready, we’ll integrate it into a &lt;strong&gt;Next.js&lt;/strong&gt; app using &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.copilotkit.ai/coagents&quot;&gt;Co Agents&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Instead of setting up everything from scratch, you can use my &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kom-senapati/coagents-starter&quot;&gt;starter kit&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;building-the-sql-agent&quot;&gt;Building the sql agent&lt;/h3&gt;
&lt;p&gt;You need to edit the &lt;code&gt;agent.py&lt;/code&gt; in &lt;code&gt;./agent/my_agent&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;Add all the imports&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```python
from langchain_community.utilities import SQLDatabase
from langchain_core.messages import AIMessage, BaseMessage
from langchain_core.runnables import RunnableLambda
from langchain_core.output_parsers import StrOutputParser
from langchain.prompts import PromptTemplate
from langchain_groq import ChatGroq
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from pydantic import BaseModel, Field
from typing import Literal, Annotated, List
from typing_extensions import TypedDict
from dotenv import load_dotenv
```

You can load environment variables using:

&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt; ```py
    load_dotenv()
    ```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need a SQLite database for the SQL agent. For this example, I’m using the &lt;a rel=&quot;nofollow&quot; href=&quot;https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db&quot;&gt;Chinook db&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We will use &lt;a rel=&quot;nofollow&quot; href=&quot;https://python.langchain.com/api_reference/community/utilities/langchain_community.utilities.sql_database.SQLDatabase.html&quot;&gt;SQLDatabase&lt;/a&gt; to create an engine for querying the database and retrieving its context.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```py
db = SQLDatabase.from_uri(r&quot;sqlite:///data/data.db&quot;)

def db_query_tool(query: str) -&amp;gt; str:
    result = db.run_no_throw(query)
    if not result:
        return &quot;Error: Query failed. Please rewrite your query and try again.&quot;
    return result

def get_context() -&amp;gt; str:
    return db.get_table_info()
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need an LLM to interact with users. Here, we’re using the Llama 3.3 model via Groq.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```py
llm = ChatGroq(model=&quot;llama-3.3-70b-versatile&quot;)
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now comes the fun part - creating AgentState, edges and nodes for our LangGraph agent.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```py
class AgentState(TypedDict):
    &quot;&quot;&quot;
    Attributes:
        messages (List[BaseMessage]): List of messages exchanged during the interaction.
        user_query (str): The user-provided query.
        sql_query (str): The generated SQL query.
        query_results (str): Results from executing the SQL query.
    &quot;&quot;&quot;

    messages: Annotated[List[BaseMessage], add_messages]
    user_query: str
    sql_query: str
    query_results: str
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need to verify if the user's query is relevant to the database.&lt;/p&gt;
&lt;p&gt;We can use structured outputs to obtain relevance from LLM using the Pydantic model by using &lt;code&gt;withured_outputs()&lt;/code&gt; method.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```py
def check_relevance(state: AgentState) -&amp;gt; Literal[&quot;YES&quot;, &quot;NO&quot;]:
    class QueryRelevance(BaseModel):
        query_relevant: str = Field(
            description=&quot;Relevance of user query to database context. The response must be 'YES' or 'NO'.&quot;
        )

    relevance_prompt = PromptTemplate.from_template(
        &quot;&quot;&quot;
        You are a relevance evaluation assistant. Your task is to determine if a user's query is relevant to a given database context. 
        Respond with 'YES' if the query is relevant, and 'NO' if it is not.

        Database Context: {context}.

        Example Queries (may not be related to context):
        1. &quot;What were the total sales in 2010?&quot; -&amp;gt; YES
        2. &quot;Who is the CEO of the company?&quot; -&amp;gt; NO
        3. &quot;Which sales agent had the highest sales in 2009?&quot; -&amp;gt; YES
        4. &quot;What is the weather today?&quot; -&amp;gt; NO

        Now evaluate the following query and respond with 'YES' or 'NO':
        {query}
        &quot;&quot;&quot;
    )

    model = llm.with_structured_output(QueryRelevance)
    relevance_generator = relevance_prompt | model

    inputs = {&quot;context&quot;: get_context(), &quot;query&quot;: state[&quot;messages&quot;][-1].content}
    relevance = relevance_generator.invoke(inputs)

    return relevance.query_relevant
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Basically we attach prompt to the model and invoke the chain with our inputs (query - user query, context - database schema info).&lt;/p&gt;
&lt;p&gt;Now, let’s define the nodes for query execution, answer generation, and handling irrelevant queries.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```py

def sql_query_execution(state: AgentState) -&amp;gt; AgentState:
    &quot;&quot;&quot;
    Executes the SQL query and retrieves the results.

    Args:
        state (AgentState): The current state of the agent.

    Returns:
        AgentState: Updated state with the SQL query results.
    &quot;&quot;&quot;
    state[&quot;query_results&quot;] = db_query_tool(state[&quot;sql_query&quot;])
    return state


def answer_generation(state: AgentState) -&amp;gt; AgentState:
    &quot;&quot;&quot;
    Generates a natural language response based on the user query and SQL query results.

    Args:
        state (AgentState): The current state of the agent.

    Returns:
        AgentState: Updated state with the generated response.
    &quot;&quot;&quot;
    answer_generation_prompt = PromptTemplate.from_template(
        &quot;&quot;&quot;
    You are a highly skilled assistant responsible for generating user-friendly answers based on a user query and the SQL query's output.
    You will use the provided SQL output to craft a clear, concise, and helpful response that directly answers the user's query.
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;     User Query: {query}

        SQL Query Output:
        {sql_output}

        Instructions:
        - Analyze the SQL query output.
        - Generate a response that answers the user query in natural language.
        - Be specific and provide numerical values or details where applicable.

        Output:
        Provide only the answer to the user query in plain text, without any additional formatting or explanation.
        &quot;&quot;&quot;
    )

    inputs = {
        &quot;query&quot;: state[&quot;user_query&quot;],
        &quot;sql_output&quot;: state[&quot;query_results&quot;],
    }

    answer_generator = answer_generation_prompt | llm
    return {**state, &quot;messages&quot;: state[&quot;messages&quot;] + [answer_generator.invoke(inputs)]}


def query_not_relevant(state: AgentState) -&amp;gt; AgentState:
    &quot;&quot;&quot;
    Handles cases where the user's query is not relevant to the database context.

    Args:
        state (AgentState): The current state of the agent.

    Returns:
        AgentState: Updated state with a message indicating irrelevance.
    &quot;&quot;&quot;
    return {
        **state,
        &quot;messages&quot;: state[&quot;messages&quot;]
        + [AIMessage(content=&quot;Given user query is not relevant to database context.&quot;)],
    }
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, let's create a workflow that compiles into the agent.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```py
workflow = StateGraph(AgentState)

workflow.add_node(&quot;sql_query_generation&quot;, sql_query_generation)
workflow.add_node(&quot;sql_query_execution&quot;, sql_query_execution)
workflow.add_node(&quot;answer_generation&quot;, answer_generation)
workflow.add_node(&quot;query_not_relevant&quot;, query_not_relevant)

workflow.add_conditional_edges(
    START, check_relevance, {&quot;YES&quot;: &quot;sql_query_generation&quot;, &quot;NO&quot;: &quot;query_not_relevant&quot;}
)
workflow.add_edge(&quot;query_not_relevant&quot;, END)
workflow.add_edge(&quot;sql_query_generation&quot;, &quot;sql_query_execution&quot;)
workflow.add_edge(&quot;sql_query_execution&quot;, &quot;answer_generation&quot;)
workflow.add_edge(&quot;answer_generation&quot;, END)
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, lets compile the workflow with memory.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```py
graph = workflow.compile(checkpointer=MemorySaver())
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's run this agent.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```bash
# in terminal
poetry install
poetry run demo
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;ui&quot;&gt;UI&lt;/h3&gt;
&lt;p&gt;In the starter kit, the &lt;code&gt;/copilotkit&lt;/code&gt; route is already set up to connect the agent to our Next.js app.&lt;/p&gt;
&lt;p&gt;On the homepage(/), I’ve just rendered a heading and the chatbot provided by CopilotKit with some custom colors.&lt;/p&gt;
&lt;p&gt;Let's run this app&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```bash
pnpm i
pnpm dev
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also add your own &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.copilotkit.ai/guides/custom-look-and-feel/headless-ui&quot;&gt;custom chatbot UI&lt;/a&gt;, as CopilotKit provides the &lt;code&gt;useCopilotChat()&lt;/code&gt; hook.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;SQL Agent Chatbot is created!&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;more-features&quot;&gt;More Features&lt;/h3&gt;
&lt;p&gt;You can add more features to this app, such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Changing the database via the ui&lt;/li&gt;
&lt;li&gt;Adding animations to the ui&lt;/li&gt;
&lt;li&gt;Displaying sql query execution results to the user&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Feel free to add anything you can imagine—there are no limits!&lt;/p&gt;
&lt;p&gt;Here’s the source code for this project: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kom-senapati/DataChat&quot;&gt;kom-senapati/DataChat&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;wrapping-it-up&quot;&gt;Wrapping it up&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/CopilotKit/CopilotKit&quot;&gt;CopilotKit&lt;/a&gt; is an innovative tool for adding AI copilots into your project. You can connect these copilots to deployed LangGraph ai agents.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/langchain-ai/langgraph&quot;&gt;LangGraph.&lt;/a&gt; is a python library that allows you to create stateful ai agents as graphs and supports multi-agent workflows.&lt;/p&gt;
&lt;p&gt;We should use these technologies to keep up with the rapidly changing landscape of the software industry.&lt;/p&gt;
&lt;p&gt;Dont forget to start my project DataChat, CopilotKit, LangGraph ⭐&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/810/creating-a-chatbot-for-sqlite-databases</guid>
<pubDate>Tue, 14 Jan 2025 19:49:46 +0000</pubDate>
</item>
</channel>
</rss>