Skip to main content

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 can trail.verify() (tamper-evidence) and trail.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
  • LangChaingate_langchain_tool(tool, …) returns a StructuredTool with the same name, description, and args schema. Drop-in.
  • LangGraphToolNode consumes a gated LangChain tool unchanged, so gate_langchain_tool is re-exported here. For a raw graph node that performs a side effect directly, use gate_node(node_fn, name=…, action=…, policy=…); its action reads the graph state.
  • OpenAI Agents SDKgate_openai_tool(function_tool, …) returns a FunctionTool that drops into Agent(tools=[…]).
  • CrewAIgate_crewai_tool(tool, …) returns a BaseTool. CrewAI runs tools synchronously; the bridge drives the async gate for you.
  • LlamaIndexgate_llamaindex_tool(tool, …) returns a FunctionTool.
  • Google ADKgate_adk_tool(tool_or_fn, …) returns a FunctionTool and 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-raises AdmissionError to 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.
  • Composemodel-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.
  • Assureanother agent you don't control (a chatbot, an endpoint, an OpenClaw-style runtime): red-team it as a Target with the core SDK's red_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tulip_frameworks-0.1.0.tar.gz (30.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tulip_frameworks-0.1.0-py3-none-any.whl (27.2 kB view details)

Uploaded Python 3

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

Hashes for tulip_frameworks-0.1.0.tar.gz
Algorithm Hash digest
SHA256 37b53c5004995029d6ec0001b6fa5a6b9a62dc0e4d87f39de464e1a4275fa6b4
MD5 6dce5ee0e888dcab7154e07be5be0941
BLAKE2b-256 ccaf6579b39c4507f1b8a85603b8186af478bd4f2892cd1ad197fa0282b8c4fc

See more details on using hashes here.

Provenance

The following attestation bundles were made for tulip_frameworks-0.1.0.tar.gz:

Publisher: _release.yml on tuliplabs-ai/tulip-frameworks

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tulip_frameworks-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for tulip_frameworks-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 60763692def054071634a4a4ddacef94a77f5547f713a59c6f05801bcf5fc846
MD5 7654443d9af94aa7986f14badb2b8788
BLAKE2b-256 5446b3b86864173d7c04e65eb45e5c1ca04e5f597f74b54556f1d284c3db4f1e

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page