Drop Tulip's control runtime (admit() gate + tamper-evident audit) into the agent framework you already use — LangChain, LangGraph, CrewAI, the OpenAI Agents SDK, LlamaIndex.
Project description
tulip-frameworks
Put Tulip's control gate around the actions your existing agent already takes — without rebuilding it on Tulip.
Tulip is an agentic framework with a control layer built
into the core: every action an agent takes runs only after it clears a policy you
write, pauses for a human when the stakes are high, and is recorded on a
tamper-evident audit trail. tulip-frameworks lets you keep the agent you already
have — in LangChain, LangGraph, CrewAI, the OpenAI Agents SDK, LlamaIndex, or
Google ADK — and add just that control layer to the tools it calls.
You wrap a tool once. From then on, when the agent decides to refund an order, disable an account, or run a deploy, the gate decides whether that action is allowed, held for a human, or denied — and writes the decision down either way.
Why this exists
An agent that can only read is easy to trust. An agent that can act — move money,
change accounts, touch production — is not, because the thing deciding to act is a
language model, and a language model can be talked into things. A prompt injection,
a confused chain of thought, or a bad retrieval can all end with the model calling
refund(order, 1_000_000).
The usual answer is "tell the model to be careful" in a system prompt. That is a guideline the model can ignore. Tulip's answer is a check in real code, outside the model, that runs between the decision and the side effect. The model can be fooled; it still can't get the action executed if your policy says no.
tulip-frameworks is how you get that check without leaving the framework you
already build in.
Install
pip install "tulip-frameworks[langchain]" # LangChain
pip install "tulip-frameworks[langgraph]" # LangGraph
pip install "tulip-frameworks[openai-agents]" # OpenAI Agents SDK
pip install "tulip-frameworks[crewai]" # CrewAI
pip install "tulip-frameworks[llama-index]" # LlamaIndex
pip install "tulip-frameworks[adk]" # Google ADK
pip install "tulip-frameworks[all]" # everything
import tulip_frameworks pulls in no framework package. Each bridge imports its
framework lazily and, if it's missing, tells you exactly which extra to install.
Quickstart — LangChain
from langchain_core.tools import tool
from tulip.control import Action, AuditTrail
from tulip_frameworks.langchain import gate_langchain_tool
from tulip_frameworks.policy_presets import action_gate_policy
@tool
def refund(order_id: str, amount_usd: float) -> str:
"Issue a customer refund."
return payments.refund(order_id, amount_usd)
trail = AuditTrail()
safe_refund = gate_langchain_tool(
refund,
# Describe the action's risk from the call's arguments.
action=lambda name, a: Action(
name=name, asset=a["order_id"],
blast_radius=1, kind="payment", environment="production",
),
policy=action_gate_policy(), # production actions → held for a human
trail=trail,
)
# Drop it into the agent exactly where `refund` went — same name, schema, description.
# agent = create_react_agent(model, tools=[safe_refund])
safe_refund is a real LangChain StructuredTool. Give it to your agent in place
of refund. Now:
- A refund in a non-production environment runs and is recorded.
- A production refund is held for a human — the function never executes; the agent gets back a structured "held for approval" result it can act on.
- Every decision lands on
trail, which you cantrail.verify()(tamper-evidence) andtrail.export_jsonl()(ship to a SIEM or warehouse).
A prompt injection that talks the model into a thousand production refunds produces a thousand held actions and zero executed ones.
The mental model
Every bridge is a thin wrapper over one primitive — gate_callable — which is the
only thing that actually calls the core SDK's admit(). Three inputs decide what
happens:
| Input | What it is | Who supplies it |
|---|---|---|
| Action | What the agent is about to do — its asset, blast_radius, environment, kind, tags. |
You, as a constant Action or a (name, kwargs) -> Action callable that reads the call's arguments. |
| Policy | The rule. action_gate_policy() gates on labels + blast radius; or bring a full ControlPolicy. |
You. |
| Trail | Where decisions are recorded, hash-chained so tampering is detectable. | Optional AuditTrail. |
The gate's output is one of three outcomes — allow, require_human, or deny — and the side effect runs only on allow. The audit record is written before a hold or denial surfaces, so a held action can never slip through as an un-recorded side effect.
Deriving the Action from the arguments is the recommended form, because risk
usually depends on them — a refund of $5 in staging is not the same action as a
refund of $50,000 in production.
Every framework, one line
The shape is identical across frameworks: pass the tool you already have, an
action, and a policy; get back a gated tool in that framework's native type.
from tulip_frameworks.langchain import gate_langchain_tool # -> StructuredTool
from tulip_frameworks.langgraph import gate_langchain_tool, gate_node
from tulip_frameworks.openai_agents import gate_openai_tool # -> FunctionTool
from tulip_frameworks.crewai import gate_crewai_tool # -> BaseTool
from tulip_frameworks.llamaindex import gate_llamaindex_tool # -> FunctionTool
from tulip_frameworks.adk import gate_adk_tool # -> FunctionTool
- LangChain —
gate_langchain_tool(tool, …)returns aStructuredToolwith the same name, description, and args schema. Drop-in. - LangGraph —
ToolNodeconsumes a gated LangChain tool unchanged, sogate_langchain_toolis re-exported here. For a raw graph node that performs a side effect directly, usegate_node(node_fn, name=…, action=…, policy=…); itsactionreads the graphstate. - OpenAI Agents SDK —
gate_openai_tool(function_tool, …)returns aFunctionToolthat drops intoAgent(tools=[…]). - CrewAI —
gate_crewai_tool(tool, …)returns aBaseTool. CrewAI runs tools synchronously; the bridge drives the async gate for you. - LlamaIndex —
gate_llamaindex_tool(tool, …)returns aFunctionTool. - Google ADK —
gate_adk_tool(tool_or_fn, …)returns aFunctionTooland preserves the original signature, so ADK builds the right function declaration.
Held or raised — your choice
When an action doesn't admit, you pick how it surfaces with mode:
-
mode="soft"(default) — the gated call returns a structured held-for-approval result the agent loop can read and react to (explain to the user, try a safer path, poll for the human decision). The run stays alive. For LLM-facing bridges the result is JSON, because a tool result has to be a string the model can see.{"status": "held_for_approval", "outcome": "require_human", "action": "refund", "asset": "ord-9", "reason": "production action needs a human"}
-
mode="raise"— the gate re-raisesAdmissionErrorto stop a deterministic pipeline cold.
Either way, the decision is on the trail first.
Out-of-band approval
A held action can be routed to a human on a side channel the agent can't reach.
Pass an ApprovalBridge and the held result carries an approval_id the agent can
poll while a person approves or denies elsewhere:
safe_refund = gate_langchain_tool(refund, action=…, policy=…, trail=trail,
approval=my_bridge)
# held result now includes: "approval_id": "appr-123",
# "next": "call approval_status(approval_id) once a human decides"
ApprovalBridge is a small structural Protocol (submit + state) with no
import-time dependency on any broker. The tulip-gateway
approval broker satisfies it; gateway_bridge(broker) adapts one when you have it.
Test your gate offline — no LLM, no network
tulip_frameworks.testing lets you assert a tool admits or holds deterministically,
without running a model:
from tulip_frameworks import gate_callable, action_gate_policy
from tulip_frameworks.testing import Spy, assert_allowed, assert_held
from tulip.control import Action
async def test_production_refund_is_held():
spy = Spy() # stands in for the real side effect
gated = gate_callable(
spy, name="refund",
action=Action(name="refund", environment="production", kind="payment"),
policy=action_gate_policy(),
)
result = await gated(order_id="ord-9")
assert_held(result, spy) # the side effect never ran; result is "held"
This is the same pattern the package's own unit tests use, so the behaviour you assert in CI is the behaviour your agent gets in production.
Three ways Tulip meets the ecosystem (don't conflate them)
This package is for the first one. It helps to see all three, because the same names people call "integrations" are actually three different relationships:
- Gate — agent frameworks whose tools take actions (LangChain, LangGraph,
CrewAI, the OpenAI Agents SDK, LlamaIndex, Google ADK). This package's
gate_*_tool. - Compose — model-call gateways (LiteLLM, Portkey): they route the model call;
Tulip gates the action. They stack, they don't compete — see
examples/litellm_two_layer.py. - Assure — another agent you don't control (a chatbot, an endpoint, an
OpenClaw-style runtime): red-team it as a
Targetwith the core SDK'sred_team().
A model-call gateway is not something you gate — it governs which model, whose key, within what budget; Tulip governs whether the action runs. Different layers; they stack cleanly.
Examples
Runnable, per-framework, under examples/:
| File | Shows |
|---|---|
langchain_refund_gate.py |
Gate a refund tool; production held for a human |
langgraph_soc_containment.py |
Gate a graph node that performs a side effect |
crewai_account_gate.py |
Gate a CrewAI tool (sync execution model) |
adk_account_gate.py |
Gate a Google ADK FunctionTool |
litellm_two_layer.py |
Compose: LiteLLM routes the model, Tulip gates the action |
Status
v0.1 ships gate bridges for LangChain, LangGraph, the OpenAI Agents SDK, CrewAI, LlamaIndex, and Google ADK, plus the framework-agnostic core, an out-of-band approval protocol, and offline testing helpers. The LangChain/LangGraph and OpenAI Agents bridges are the most exercised, including end-to-end tests against a real LLM; CrewAI, LlamaIndex, and ADK follow the identical pattern.
One-way dependency on tulip-agents (the same direction as the
langchain-core / langchain-community split). Apache-2.0.
→ Full docs: https://tulipagents.ai/integrations/frameworks/ · Core SDK: https://github.com/tuliplabs-ai/sdk-python
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tulip_frameworks-0.1.0.tar.gz.
File metadata
- Download URL: tulip_frameworks-0.1.0.tar.gz
- Upload date:
- Size: 30.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37b53c5004995029d6ec0001b6fa5a6b9a62dc0e4d87f39de464e1a4275fa6b4
|
|
| MD5 |
6dce5ee0e888dcab7154e07be5be0941
|
|
| BLAKE2b-256 |
ccaf6579b39c4507f1b8a85603b8186af478bd4f2892cd1ad197fa0282b8c4fc
|
Provenance
The following attestation bundles were made for tulip_frameworks-0.1.0.tar.gz:
Publisher:
_release.yml on tuliplabs-ai/tulip-frameworks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tulip_frameworks-0.1.0.tar.gz -
Subject digest:
37b53c5004995029d6ec0001b6fa5a6b9a62dc0e4d87f39de464e1a4275fa6b4 - Sigstore transparency entry: 1970100803
- Sigstore integration time:
-
Permalink:
tuliplabs-ai/tulip-frameworks@c994a33eec42eb177a84e8b4d0abc9ca991e0a54 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/tuliplabs-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
_release.yml@c994a33eec42eb177a84e8b4d0abc9ca991e0a54 -
Trigger Event:
release
-
Statement type:
File details
Details for the file tulip_frameworks-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tulip_frameworks-0.1.0-py3-none-any.whl
- Upload date:
- Size: 27.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
60763692def054071634a4a4ddacef94a77f5547f713a59c6f05801bcf5fc846
|
|
| MD5 |
7654443d9af94aa7986f14badb2b8788
|
|
| BLAKE2b-256 |
5446b3b86864173d7c04e65eb45e5c1ca04e5f597f74b54556f1d284c3db4f1e
|
Provenance
The following attestation bundles were made for tulip_frameworks-0.1.0-py3-none-any.whl:
Publisher:
_release.yml on tuliplabs-ai/tulip-frameworks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tulip_frameworks-0.1.0-py3-none-any.whl -
Subject digest:
60763692def054071634a4a4ddacef94a77f5547f713a59c6f05801bcf5fc846 - Sigstore transparency entry: 1970100881
- Sigstore integration time:
-
Permalink:
tuliplabs-ai/tulip-frameworks@c994a33eec42eb177a84e8b4d0abc9ca991e0a54 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/tuliplabs-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
_release.yml@c994a33eec42eb177a84e8b4d0abc9ca991e0a54 -
Trigger Event:
release
-
Statement type: