Skip to content

SDKs

LangChain and LangGraph

A guarded tool asks Immiscible before it runs and settles after. It keeps its name, description and schema, so it drops into ToolNode, bindTools / bind_tools and the prebuilt agents unchanged.

Tested against @langchain/core 1.2 with @langchain/langgraph 1.4, and langchain-core 1.6 with langgraph 1.2, through a real ToolNode.

#TypeScript

Shell
npm install @immiscible/sdk @langchain/core @langchain/langgraph zod
TypeScript
import { tool } from '@langchain/core/tools';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';
import { Immiscible } from '@immiscible/sdk';
import { guardLangChainTools } from '@immiscible/sdk/langchain';

const immiscible = new Immiscible().run({ client: 'langchain' });

const buy = tool(async ({ pence, domain }) => placeOrder(pence, domain), {
  name: 'buy', description: 'Buy groceries', schema: z.object({ pence: z.number().int(), domain: z.string() }),
});

const tools = guardLangChainTools([buy, search], {
  client: immiscible,
  mapToAction: ({ name, args }) => name === 'search' ? null
    : Immiscible.paymentAction({ amount: args.pence, currency: 'GBP', merchant: args.domain, provenance: [{ source: 'user' }] }),
});

const model = new ChatOpenAI({ model: 'gpt-5-mini', configuration: immiscible.gateway.openai() }).bindTools(tools);
const toolNode = new ToolNode(tools);

#Python

Shell
python3 -m venv .venv && . .venv/bin/activate
pip install immiscible langchain-core langgraph
Python
from langchain_core.tools import tool
from langgraph.prebuilt import ToolNode
from immiscible import Immiscible
from immiscible.integrations import guard_langchain_tools

immiscible = Immiscible().run(client="langchain")

@tool
def buy(pence: int, domain: str) -> str:
    """Buy groceries from a supermarket."""
    return place_order(pence, domain)

tools = guard_langchain_tools(
    [buy, search],
    client=immiscible,
    map_to_action=lambda call: None if call.name == "search"
        else Immiscible.payment_action(call.args["pence"], "GBP", call.args["domain"], provenance=[{"source": "user"}]),
)
node = ToolNode(tools)                       # in a StateGraph, or create_react_agent(model, tools)

Both invoke and ainvoke are guarded; the async path makes its HTTP calls in a worker thread. When ToolNode hands over a tool call, a refusal comes back as a ToolMessage with status="error" and the tool call’s id, as ToolNode requires. Point a chat model at the gateway with ChatOpenAI(**immiscible.gateway.openai()) (base URL, key and headers) or ChatOpenAI(http_client=...) with the run’s hooks.

You can also guard the function under @tool:

Python
from immiscible.integrations import guarded

@tool
@guarded(to_action, client=immiscible)
def buy(pence: int, domain: str) -> str:
    """Buy groceries from a supermarket."""

#A person in the loop

The default waits for the person inside the tool call, up to ten minutes. In a LangGraph graph with a checkpointer you may prefer to pause the graph instead:

  • wait: false / wait=False: refuse at once with “waiting for the person to approve”; route on it, and retry the tool call after approval with the same tool call id (it is the idempotency key, so the retry finds the same action, now allowed).
  • onApprovalRequired / on_approval_required: call LangGraph’s interrupt({ approve: d.approval.url }) there to suspend the graph until the person answers.

Options are the same as for the OpenAI Agents SDK.

Examples: packages/immiscible-js/examples/langchain.mjs, packages/immiscible-py/examples/langchain_example.py. Both run against the fake with --demo.