LangGraph Tool Execution: LangChain vs External Agent Harness β€” Trade Space

Status: ACTIVE (conversation observation) Agent: opencode/ext-agent (sandshrew) Timestamp UTC: 2026-05-11T18:55:00Z Claim: synthesis | 2026-05-11T18:50:00Z Session: MjF trade-off exploration β€” LangChain tools vs Pi/Hermes agents inside LangGraph nodes

Prior Context

The Core Tension (As Observed)

Two paths for tool execution inside LangGraph nodes:

Path Pro Observed Con Observed
LangChain tools (bind_tools, ToolNode, tool schemas) Native integration with LangGraph's patterns. Tools are visible in state, checkpointable, interruptible. Documented, tested, familiar to LangGraph users. Every tool must be declared upfront at compile time. Adaptive tool use at runtime requires anticipating every possible tool schema. If a situation emerges that needs a tool not in the schema, the node stalls.
External agent harness (Pi, Hermes, raw function calls) Already has tool calls, multimodal abilities, adaptive reasoning. No pre-staging every tool schema β€” agent decides what to call at runtime. Lightweight (Bun runtime for Pi agents). Internal tool calls are opaque to LangGraph state. LangGraph only sees the agent's final output, not intermediate tool calls. Wiring pattern may feel less "native" β€” node is a black-box wrapper around an external process.

What LangGraph Actually Cares About

LangGraph is an orchestration runtime. It cares about three things: 1. State β€” what keys exist and how updates merge 2. Nodes β€” functions that receive state and return updates 3. Edges β€” how the next node is chosen

LangGraph does NOT care how a node produces its update. The official docs state: "You don't need to use LangChain to use LangGraph." Nodes are plain Python functions. Inside a node, the developer can call an LLM directly, invoke a LangChain tool, shell out to a Bun process, make an HTTP request to a Pi agent, or run pure Python logic. LangGraph is indifferent.

This means LangChain tools are NOT the native convention β€” they are ONE option among many. The node's internal implementation is LangGraph's blind spot by design.

Observed Boundary: What Granularity of State Tracking Is Needed?

The choice hinges on how much of the tool execution pipeline needs to be visible in LangGraph state:

Need LangChain Tools External Harness
Track every tool call in state Yes β€” ToolNode writes tool calls/results to state No β€” internal calls invisible
Checkpoint mid-tool-execution Yes β€” each tool call is a super-step No β€” agent black box
Interrupt after a specific tool Yes β€” interrupt() between tools No β€” agent runs to completion
Adaptive tool selection at runtime Only tools in schema Full adaptivity β€” agent decides
Multimodal (image, audio) Requires custom tool definitions Built into Hermes-style harnesses
Lightweight runtime Python only Bun (Pi agents) or any
Pre-staging burden High β€” every tool schema upfront Low β€” agent handles its own tools

Observed Patterns for Bridging

Pattern A: Black Box

def hex_node(state: State):
    """LangGraph owns routing + state. Agent handles everything inside."""
    agent = get_agent("hermes")
    result = agent.run(
        task="Unit at hex_17 with enemies nearby. Decide and act.",
        context={"state": state, "unit": state["unit"]}
    )
    return {"last_action": result.action, "unit": result.updated_state}

LangGraph asks "what should we do here?" The agent answers. LangGraph tracks the outcome, not the process. LangGraph state only sees: input β†’ agent decision β†’ output. Intermediate tool calls (the agent deciding to scout, then move, then attack) are invisible to LangGraph β€” they're the agent's internal reasoning loop.

Pattern B: Full Visibility

# Each tool is a LangChain tool, each call is a state checkpoint
graph.add_node("hex_node", ToolNode([
    move_tool, attack_tool, scout_tool, use_item_tool
]))

LangGraph sees every tool invocation. State tracks: unit requested move, state updated with new position, next tool scout fires. Every step is checkpointable and interruptible. But every tool must be in the schema at compile time.

Pattern C: Hybrid β€” LangGraph Routes, Agent Decides

def hex_node(state: State, runtime: Runtime):
    """LangGraph provides situation. Agent decides which tools to use.
    Agent reports back high-level outcomes. LangGraph tracks those."""

    # LangGraph sends structured situation to agent
    situation = {
        "turn": state["turn"],
        "unit": state["unit"],
        "nearby": state["nearby_units"],
        "terrain": state["terrain"][state["unit"]["position"]],
        "objectives": state["objectives"],
        "available_tools": ["move", "attack", "scout", "fortify"]
    }

    # Agent (Pi/Hermes) decides internally, uses its own tools
    agent = runtime.context.get_agent("pi")
    decision = agent.decide(situation)

    # LangGraph tracks the decision at a high level
    return {
        "unit_action": decision.action,       # "attack hex_18"
        "unit_target": decision.target,
        "action_log": decision.reasoning,      # agent's internal reasoning, logged
        "unit_state": decision.unit_update     # health deltas, position, etc.
    }

LangGraph owns the graph topology, state schema, routing, and checkpoints. The agent owns tool selection and execution. The interface is a structured handoff: "here's the situation and available tools" β†’ "here's what I decided and what changed." LangGraph never needs to know whether the agent called scout before attack β€” it only needs the outcome.

This avoids both problems: - No pre-staging every tool in LangChain format - No fighting LangGraph's routing β€” routing remains LangGraph's job - Agent adaptivity is preserved β€” it can chain tools or invent new approaches

Observed: Runtime Separation Concern

Pi agents run on Bun (JavaScript). LangGraph runs on Python. If both run on the same Pi:

Same-machine options: - LangGraph Python node β†’ subprocess call to Bun β†’ Pi agent runs β†’ returns JSON β†’ LangGraph resumes - LangGraph Python node β†’ HTTP call to local Pi agent server β†’ JSON response - LangGraph threads and Pi agent as separate OS processes, state passed via JSON

Cross-machine options: - LangGraph on Pi, Pi agent HTTP server on Pi β†’ simplest - LangGraph on Mac, Pi agent on Pi β†’ network calls, need Tailscale or LAN

The subprocess pattern (Python calling Bun) is the tightest coupling but also the least overhead. The HTTP pattern is cleaner separation but adds a local server that must stay running.

Open Questions (Not Prescriptive)