@kblockai/client/langchain turns the tools the Gateway lists for your agent into LangChain.js tools. Every call still goes through the Gateway, so policies, approvals and receipts apply as with the TypeScript client; the adapter only translates each outcome into something a LangChain agent understands.
Use it when your agent already runs on LangChain.js (@langchain/core). You pass the tools to your agent as you would any other tools; the model reads K-Block's explanation when a call is held or denied, and your code gets the typed result.
Install
The adapter is a subpath of @kblockai/client, not a separate package. It needs @langchain/core next to it:
npm install --save-exact @kblockai/client@0.1.0-rc.1
npm install @langchain/core@^1| Dependency | Version |
|---|---|
@kblockai/client | 0.1.0-rc.1 (Node.js 22 or later) |
@langchain/core | Optional peer dependency, >=1.0.0 <2. K-Block's own tests use 1.2.13. |
@langchain/core is optional for the main entry point: you only need it if you import @kblockai/client/langchain.
What it provides
| Export | What it does |
|---|---|
loadKBlockTools(client, options?) | Lists the agent's tools on the Gateway and returns one LangChain tool per tool. With options.tools, only those names; a name the Gateway does not list throws a TypeError. |
kblockTool(client, definition, options?) | Wraps one tool definition (an entry of client.listTools()) as a LangChain tool. |
KBlockLangChainOptions | The options type (below). |
Each tool is a DynamicStructuredTool whose name, description and input schema are exactly what the Gateway lists (a tool listed without a description gets its name as description). It calls client.callTool and answers with responseFormat: "content_and_artifact":
- content: text the model can act on, from
explainResult: the tool's output when the call completed; forapproval_pending, that nothing ran yet and that the same call must be repeated after approval; fordenied, the code, detail and the policy's explanation. - artifact: the typed
KBlockCallResult(see Outcomes), available asToolMessage.artifactwhen the tool is invoked with a tool call.
Load and run the tools
This complete program loads the tools and runs one the way an agent executor runs a model's tool call, so you can try it without an LLM. Like every example on this page, it reads the Gateway URL and the agent's gateway API key from KBLOCK_GATEWAY_URL and KBLOCK_API_KEY, two environment variables of its own:
import { connectKBlock, type KBlockCallResult } from "@kblockai/client";
import { loadKBlockTools } from "@kblockai/client/langchain";
const client = await connectKBlock({
gatewayUrl: process.env.KBLOCK_GATEWAY_URL ?? "", // e.g. https://gateway.example.com
apiKey: process.env.KBLOCK_API_KEY ?? "", // the agent's gateway API key
});
// Every tool the Gateway lists for this agent. Pass `tools` to your agent.
const tools = await loadKBlockTools(client);
console.log(tools.map((t) => t.name));
// For example, a refund tool that your policy holds for approval above some amount.
const refund = tools.find((t) => t.name === "refund");
if (refund !== undefined) {
// What an agent executor does with a model's tool call:
const msg = await refund.invoke({
name: refund.name,
args: { order_id: "ord_123", amount: "250.00", currency: "USD" },
id: "call_1",
type: "tool_call",
});
const result = msg.artifact as KBlockCallResult;
console.log(msg.status, result.status); // "error" "approval_pending" when the call is held
console.log(String(msg.content)); // the explanation the model reads
}
await client.close();When you invoke a tool with a tool call (as an agent does), it returns a ToolMessage. When you invoke it with plain arguments (await refund.invoke(args)), it returns only the text.
Refusals are tool errors
Every outcome other than completed (held for approval, denied, rejected, expired, cancelled, unavailable, an upstream that failed…) comes back, for a tool call, as a ToolMessage with status: "error". Code that routes on LangChain's own flag (LangGraph edges, evaluations, tracing) therefore never counts a refusal as a successful tool run. The model still reads the explanation as the message content, and the artifact keeps the exact K-Block status.
| Option | Effect |
|---|---|
| default | Refusals invoked with a tool call: ToolMessage with status: "error"; completed calls: status: "success". |
refusalsAsErrors: false | Refusals get status: "success" too (the artifact still says what happened). |
throwOnRefusal: true | Every refusal throws KBlockRefusalError (its result is the refusal, its code the refusal's code). |
throwOnRefusal: (r) => boolean | Throw only for the refusals the predicate selects. |
Transport errors (KBlockAuthError, KBlockRateLimitedError, KBlockUnavailableError, KBlockHttpError, KBlockProtocolError) throw, whatever the options, with one exception for agents: a call whose outcome is unknown (KBlockUnavailableError with outcomeUnknown: true, or KBlockProtocolError) invoked with a tool call is answered with a ToolMessage with status: "error" whose text says that the action may or may not have run and must not be repeated blindly. Its artifact is a KBlockUnknownOutcome (kblock_status: "outcome_unknown", code, tool, idempotency_key). Agent frameworks hand a thrown error to the model with an invitation to fix and retry, and a repeated call without an idempotency key is a new request that may run the action again. Invoked with plain arguments, the error is thrown. See Errors.
import { KBlockRefusalError, connectKBlock } from "@kblockai/client";
import { loadKBlockTools } from "@kblockai/client/langchain";
const client = await connectKBlock({
gatewayUrl: process.env.KBLOCK_GATEWAY_URL ?? "",
apiKey: process.env.KBLOCK_API_KEY ?? "",
});
// For code that runs the tools itself: refusals are thrown instead of answered.
const [refund] = await loadKBlockTools(client, { tools: ["refund"], throwOnRefusal: true });
try {
console.log(await refund!.invoke({ order_id: "ord_123", amount: "2500.00", currency: "USD" }));
} catch (e) {
if (e instanceof KBlockRefusalError) console.log("refused:", e.code, e.result.status);
else throw e;
}
await client.close();Approvals and repeated calls
The adapter's defaults differ from client.callTool in one important way: it sends no idempotency key unless you ask for one.
- When a call is held, the model reads that nothing ran and that it should call the tool again with exactly the same arguments after approval. An approver approves in the dashboard (Approvals, then Approve and sign).
- When the model repeats the identical call (same tool, same arguments), the Gateway finds the open action by the call's fingerprint and executes it once it is approved.
- Once that call has executed, an identical keyless call is a new request: the policy decides it again, so a call that needed approval needs a new approval and runs nothing until then.
| Option | Default | Description |
|---|---|---|
tools | every listed tool | Load only these tool names (loadKBlockTools only). |
waitSeconds | 0 | kblock/wait-seconds for every call: how long the Gateway may hold a pending call before answering (whole seconds, 0–600, else a RangeError when the tool runs; capped by the Gateway, 60 seconds by default). |
idempotencyKey | none | (toolName, args) => string | undefined: a key per call. Use it when repeats must map to one action; the text of a held call then names that key. |
throwOnRefusal | false | See Refusals are tool errors. |
refusalsAsErrors | true | See Refusals are tool errors. |
With waitSeconds set, the model's repeat can wait for the decision instead of returning approval_pending again at once. A long wait keeps the agent's turn open for that long.
What can go wrong
| Symptom | Cause | Fix |
|---|---|---|
TypeError: the Gateway does not list 'x' for this agent… | The tool is unknown, disabled, not authorized, or its action is not granted by the agent's delegation. | Check the tool and the agent's delegation; see Gateway and tools. |
The model keeps getting approval_pending after approval | It changed the arguments, or it repeats a call that already executed without a key (a new request). | Repeat exactly the same arguments; use idempotencyKey for stable repeats. |
| A refusal counted as success in your graph | refusalsAsErrors: false is set, or the tool was invoked with plain arguments (no ToolMessage). | Keep the default and invoke with tool calls, or read artifact.status. |
Cannot resolve @langchain/core | The optional peer dependency is not installed. | npm install @langchain/core@^1. |