SDK reference /Framework adapters

Framework adapters

Adapters gate the tool boundary inside the framework that drives your agent, so you do not decorate every call by hand. Add one wrapper or one callback and every tool the agent runs is captured, checked against your policy, signed, and audited before any side effect.

An adapter hooks the one moment that matters: just before the agent runs a tool. It routes that call through the shared capture core, where it is evaluated against your policy, signed into a receipt, and appended to the audit chain. A blocked or suspended call never executes. Every framework is an optional dependency: import heso pulls in none of them, and each adapter imports its framework only when you reach for it.

Python adapters

Each adapter is a lazy submodule under heso. After a bare import heso you reach them as heso.crewai, heso.openai_agents, heso.claude_agent, heso.mcp, heso.pydantic_ai, and heso.langgraph; the LangChain callback handler is exposed directly as heso.HesoCallbackHandler. All of them read the active heso.init() config on every call, so the same workflow, account, and observe-only switch apply across the adapters and the decorators.

LangChain

What it gates: every tool a LangChain agent runs. HesoCallbackHandler gates the call on on_tool_start and binds its result on on_tool_end. Pass it in the callbacks= list your agent already accepts; a blocked or suspended action raises, so LangChain aborts the tool.

agent.pypython
import heso

heso.init()

# every tool the agent runs is gated, signed, and audited at the boundary
agent.invoke(..., config={"callbacks": [heso.HesoCallbackHandler()]})

See the LangChain guide for the full walkthrough.

LangGraph

What it gates:the same callback already gates LangGraph tool nodes, since LangGraph shares LangChain’s callback system. The heso.langgraph adapter adds the durable human-approval bridge: heso_interrupt_for_approvaldrives heso’s suspend/resume and wires it to LangGraph’s interrupt(). It returns interrupt while a human is pending, approved once the decision is on the chain, or denied on a terminal refusal.

graph.pypython
from heso.langgraph import heso_interrupt_for_approval

def high_stakes_node(state):
    # parks the graph for a human; resumes from the signed chain once approved
    status = heso_interrupt_for_approval(spec, session=state["session"])
    if status["status"] == "approved":
        return run_the_action(state)
    return {"refused": status}

CrewAI

What it gates: each CrewAI tool call, through its hook pair. A CrewAI tool carries a before_hook that aborts the call by returning False and an after_hook that observes the result. The adapter maps the policy verdict onto that boolean (allowed runs, blocked or suspended skips) and binds the result as a follow-up receipt.

agent.pypython
from heso.crewai import heso_before_hook, heso_after_hook

# before_hook gates the call (return False aborts); after_hook binds the result
my_tool.before_hook = heso_before_hook
my_tool.after_hook = heso_after_hook

OpenAI Agents SDK

What it gates: each function-tool call, through a tool input guardrail. heso_tool_guardrail()returns the SDK’s own verdict object: allow() on an allowed outcome, reject_content(reason) on a blocked or suspended one. HesoAgentHooks adds the result-bound receipt.

agent.pypython
from agents import Agent, function_tool
from heso.openai_agents import heso_tool_guardrail, HesoAgentHooks

@function_tool(tool_input_guardrails=[heso_tool_guardrail()])
def search(query: str) -> str:
    ...

# the guardrail enforces; the hooks add the result-bound receipt
agent = Agent(name="researcher", tools=[search], hooks=HesoAgentHooks())

See the OpenAI & Anthropic guide for setup and policy examples.

Claude Agent SDK

What it gates: each tool call, through a PreToolUse hook that returns a permission decision (allow, deny for a policy block, or ask for a human suspension) plus a PostToolUse hook that records the result. heso.claude_agent.hooks() builds the matcher config wiring both.

agent.pypython
import heso
from claude_agent_sdk import ClaudeAgentOptions

heso.init()

# PreToolUse gates (allow / deny / ask); PostToolUse binds the result
options = ClaudeAgentOptions(hooks=heso.claude_agent.hooks())

See the OpenAI & Anthropic guide for the full walkthrough.

PydanticAI

What it gates: each call to a wrapped tool callable. PydanticAI tools are plain functions, so gate_toolwraps the callable, binds the call’s arguments to their parameter names, and gates each invocation before the body runs; a blocked or suspended action raises. Parameterize the verb (gate_tool(verb=Verb.DELETE)) to floor a destructive tool into the delete lane.

agent.pypython
from pydantic_ai import Agent
from heso.pydantic_ai import gate_tool

@gate_tool
def transfer_funds(amount: int, to: str) -> str:
    return move_money(amount, to)

agent = Agent("openai:gpt-4o", tools=[transfer_funds])

MCP servers

What it gates: every tools/call inside an MCP server you own. heso.mcp.wrap_call_tool wraps the call_tool(name, arguments) dispatch the SDK registers, so each invocation is gated on the way in and receipted on the way out. For a server you do not control, the zero-code heso-mcp proxy wraps the whole server command instead.

server.pypython
import heso
from mcp.server import Server

server = Server("my-tools")

@server.call_tool()
@heso.mcp.wrap_call_tool
async def handle(name, arguments):
    ...

TypeScript adapters

The Node SDK ships two tool-gating adapters under the @hesohq/sdk/adapters/* subpath. Both gate beforethe tool’s execute runs, and a blocked or suspended action throws, which each framework turns into a tool-error the model sees, so the body never runs ungated. On allow, the result is bound to a follow-up receipt. Neither imports its framework, so both stay optional dependencies.

Vercel AI SDK

What it gates:tools in a Vercel AI SDK v5 tools record. The SDK does not carry a tool’s name on the tool object (the name is the record key it is registered under), so gateTool(name, tool) takes the name from the caller, and gateTools(record) gates a whole record by its keys.

agent.tsts
import { streamText } from 'ai'
import { gateTool, gateTools } from '@hesohq/sdk/adapters/ai-sdk'

// gate one tool by the name the model calls...
const search = gateTool('search', tool({ inputSchema, execute }))

// ...or gate a whole tools record by its keys
streamText({ model, tools: gateTools({ search, sendEmail }) })

Mastra

What it gates: a Mastra tool’s execute. A Mastra tool carries its name in id, so gateTool(tool)reads it from there and unwraps the input from Mastra’s execute argument (its context or inputData, or the argument itself).

agent.tsts
import { createTool } from '@mastra/core/tools'
import { gateTool } from '@hesohq/sdk/adapters/mastra'

const search = gateTool(
  createTool({
    id: 'search',
    description: 'Search the web',
    inputSchema,
    execute: async ({ context }) => runSearch(context),
  }),
)
Adapters gate the tool boundary, not the model

Every adapter gates what the agent does— the tools it runs and the arguments it passes — not the text the model writes. A receipt proves you authorized this action under your policy, and at L1that a person approved it. It does not prove the action succeeded downstream, and it does not prove the model’s reasoning was sound.

Next steps