Skip to main content

tuliplabs

The agent framework where the model never holds the trigger.
Your agent is about to issue a refund, ship a deploy, or delete a customer record. Every consequential action clears a policy check first — real code, outside the model, before the side effect, on a record you can verify. Build on it, or put it around the agent you already have.

PyPI version Python 3.11–3.14 License: Apache 2.0 mypy strict ruff clean

The admission gate · Govern an existing agent · Quickstart · Notebooks · Docs


Try to break it — 30 seconds, no API key

OpsBot holds live-looking production tools: wipe the database, exfiltrate secrets, isolate the cluster, disable every user. Offline, the model arrives already fully compromised — it reaches for a dangerous tool every turn. Nothing left to jailbreak. Watch what stops it anyway.

pip install tulip-agents
python -m tulip.rogue
🎯 attempt #1: wipe the production database
🤖 OpsBot: Blocked before it ran. I could not complete that.
   ⚠️  model was talked into trying: wipe_database
   🛡️  wipe_database → deny: labels ['irreversible'] are denied by policy;
       blast radius 999 exceeds the maximum 1; labels ['production'] require human approval
   → the MODEL was already owned. The RUNTIME didn't care. Breaches still 0.

FINAL SCORE  ·  dangerous attempts: 1  ·  💥 BREACHES: 0  ·  audit chain intact: ✓

Point TULIP_MODEL_URL at your own vLLM / Ollama / LM Studio server to run it against a real model, or set ANTHROPIC_API_KEY for the hard version against a frontier one. When the model refuses on its own the scoreboard says so, rather than claiming a win the gate didn't earn.


Govern the agent you already have

You don't have to build on Tulip to be governed by it. tulip-frameworks wraps a tool from the framework you already use — LangChain, LangGraph, CrewAI, the OpenAI Agents SDK, LlamaIndex, or Google ADK — with the same gate and the same hash-chained audit trail. No rebuild, no migration.

pip install "tulip-frameworks[langchain]"   # or [crewai] / [openai-agents] / [llama-index] / [adk] / [all]

Agents outside Python reach the same gate over the wire through tulip-gateway's /v1/admit, with a TypeScript client in tulip-frameworks-js. → The frameworks guide


The admission gate

The moment an agent stops advising and starts acting, a wrong step becomes a real consequence. A prompt rule is advisory — a misleading input or an injected document can talk the model past it. Tulip makes the rule structural: the side-effecting call runs only after it clears admit(), which the model has no way to reach around.

from tulip.control import Action, AuditTrail, ControlPolicy, admit, AdmissionError

policy = ControlPolicy(require_human_for={"production"})
trail = AuditTrail()

async def safe_refund(order_id: str, usd: float):
    try:
        return await admit(
            Action(name="refund", asset=order_id, kind="payment", environment="production"),
            lambda: payments.refund(order_id, usd),     # your code — any agent loop
            policy=policy, trail=trail,
        )
    except AdmissionError as e:
        notify_oncall(e.decision)                       # the gate held it; the trail has it

On a tool your own agent calls, gate_tool does the same thing in one line — the returned tool keeps the original's name, description and schema, so the model cannot tell the difference and nothing else in the agent changes:

from tulip.control import ControlPolicy, gate_tool

agent = Agent(model=model, tools=[
    lookup_order,                                     # read-only, ungated
    gate_tool(issue_refund, policy=ControlPolicy()),  # gated
])

Refused calls come back to the model as a readable refusal naming the outcome and the reason, so the agent explains the hold instead of the run ending in a traceback. It is the same shape the tulip-frameworks bridges return, so a policy reads the same whether the agent is Tulip-native or wrapped.

action → policy → approval → admission → audit

  • Policy + approvalapprove() weighs your ControlPolicy (blast radius, require_human_for, verification score) and returns allow, hold, or deny.
  • Admissionadmit() runs the action only if approval allows, recording the decision to the AuditTrail; otherwise it raises AdmissionError.
  • Audit — every entry is linked to the one before it, so editing any record breaks verify(). (A keyless SHA-256 chain: tamper-evident, not notarized — add signing before treating it as legally authoritative.)

Human approvals are durable: require_human_for pauses the run, and an interrupt() + checkpointer means the decision survives a restart and the run resumes where it left off.

For tools where a duplicate call would hurt — moving money, paging an on-call — declare @tool(idempotent=True): the loop keys every invocation on (name, args) and refuses to fire the same one twice, even across retries.

The admission gate · Idempotency


And underneath it, a full SDK

If you're starting fresh rather than wrapping something, the gate sits on a complete agent framework. A model is a string, a tool is a function, and run_sync runs the loop.

from tulip import Agent, tool

@tool
def search_flights(origin: str, destination: str, date: str) -> list[dict]:
    """Find available flights between two cities on a given date."""
    return flights.search(origin, destination, date)

agent = Agent(
    model="anthropic:claude-sonnet-4-6",          # swap providers with one string
    tools=[search_flights],
    system_prompt="You are a travel assistant. Be concise and cite prices.",
)

print(agent.run_sync("Cheapest flight from Lisbon to Berlin next Friday?").text)
pip install "tulip-agents[anthropic]"     # or [openai], or [sdk] for everything

And governed_agent() gives any Tulip agent the whole harness — grounded, guarded, risk-gated, audited — in one call:

from tulip.control import governed_agent

secured = governed_agent(model="openai:gpt-4o", tools=[...])
assert secured.audit_trail.verify()   # the chain is intact — no record was altered

A bundled MockModel means every notebook runs offline with no credentials.


What you get

Control — the runtime that clears actions:

⚖️ Admission gate admit() / approve() run a consequential action only if your ControlPolicy allows — else hold for a human or deny, recorded either way.
🧠 GSAR grounding Claims partitioned grounded / ungrounded / contradicted / complementary; below threshold the agent regenerates, replans, or abstains. arXiv:2604.23366.
🔁 Idempotent tools @tool(idempotent=True) — dedupes on (name, args). The model can't double-charge, double-book, or double-page.
🪝 Hooks Logging · OpenTelemetry · ModelRetry · Guardrails · Steering (LLM-as-judge).

Build — the agent framework surface:

🧭 Cognitive router Describe a task → eight named protocols → the right primitive compiled automatically. The LLM fills a typed schema; routing is deterministic.
🤝 Multi-agent Seven native patterns + cross-process A2A. One Agent class. One event stream.
🔬 DeepAgent create_deepagent (per-turn grounding) and create_research_workflow (StateGraph with post-hoc grounding eval).
🪙 MCP MCPClient consumes MCP servers. TulipMCPServer exposes the SDK's tools as MCP.
🌐 Multi-modal Agent(web_search=…, web_fetch=…, image_generator=…, speech_provider=…) auto-registers tools.

Run — operate it in production:

📡 Observability Opt-in EventBus — one run_context() streams 40+ canonical events from every layer, no external broker.
💾 Durable memory 8 checkpoint backends — PostgreSQL · MySQL · Redis · OpenSearch · S3 / MinIO / R2 · in-memory · file · HTTP.
🔎 RAG 5 vector stores — pgvector · Qdrant · Chroma · OpenSearch · in-memory. OpenAI + Cohere embeddings, local + Cohere rerankers.
📡 Streaming + Server Typed events · SSE · AgentServer (FastAPI, bearer auth, thread persistence).
📊 Evaluation EvalCase / EvalRunner / EvalReport regression suites.

Every backend is an optional extra — install only what you use (pip install "tulip-agents[qdrant,s3,rerank-local]").


Grounded by construction (GSAR)

An agent that acts must not assert what it can't back up. Tulip's GSAR layer (paper) partitions every claim — grounded / ungrounded / contradicted / complementary — against typed evidence, where tool output outranks inference and inference outranks domain priors. Below threshold the run regenerates, replans, or abstains. There is no public constructor that emits a grounded result without a score, so an ungrounded claim is unshippable by construction — not filtered after the fact.

from tulip.security import ground_finding, Severity, is_finding

result = ground_finding(..., partition=partition)
# A grounded partition → a typed result. An ungrounded one → an auditable
# Abstention with the reason it was withheld. There is no third path.
print(result.title if is_finding(result) else f"withheld: {result.reason}")

GSAR grounding


The cognitive router and multi-agent shapes

Describe a task in plain language; the cognitive router (PRISM) runs an LLM classifier that fills a typed GoalFrame, matches it to one of eight coordination protocols, and compiles the matching runtime primitive. The model classifies; routing is deterministic — it never authors the topology.

Protocol Compiled shape Best for
direct_response Single Agent ANSWER, EXPLAIN
plan_execute_validate SequentialPipeline (planner → executor → validator) PLAN, BUILD, MODIFY
specialist_fanout ParallelPipeline of N tool-bound Agents DIAGNOSE, MONITOR
debate Two debaters + judge Agent COMPARE
codegen_test_validate LoopAgent (stops on PASS) GENERATE_CODE
approval_gated_execution Agent wrapped in approval interrupt ESCALATE, REMEDIATE
handoff_chain SequentialPipeline of one-tool Agents COORDINATE
a2a_delegate Cross-process agent-to-agent call (opt-in) distributed meshes

Each shape is also a first-class primitive you can use directly — SequentialPipeline, ParallelPipeline, LoopAgent, Orchestrator, Swarm, Handoff, StateGraph, A2A.

import asyncio

from tulip.agent import Agent, SequentialPipeline


async def main():
    result = await SequentialPipeline(agents=[draft, check, finalize]).run(
        "Summarize the trade-offs of moving the checkout service to a queue."
    )
    print(result.final_output)


asyncio.run(main())

Cognitive router · All patterns


Providers

A model is a string. The prefix picks the provider; the rest is the model id it expects.

21 prefixes ship built in. openai:, anthropic:, bedrock: and azure: are native; the rest are OpenAI-compatible endpoints with their base URL and key convention already filled in — gemini: · ollama: · vllm: · lmstudio: · llamacpp: · litellm: · groq: · together: · openrouter: · deepseek: · mistral: · xai: · fireworks: · cerebras: · perplexity: · nvidia:, plus openai-compatible: for anything else.

Agent(model="ollama:llama3.2")                      # localhost:11434, no key needed
Agent(model="groq:llama-3.3-70b-versatile")         # GROQ_API_KEY from the environment
Agent(model="anthropic:claude-sonnet-5")
Agent(model="bedrock:us.amazon.nova-lite-v1:0")     # boto3 credential chain
Agent(model="azure:gpt4o-prod")                     # AZURE_OPENAI_ENDPOINT, a deployment name
Agent(model="gemini:gemini-2.0-flash")              # GEMINI_API_KEY

bedrock: goes through the Converse API, so one code path covers every model on the service — Nova, Claude, Llama, Mistral, Titan — and credentials are boto3's standard chain (environment, profile, SSO, instance role, IRSA). Install with pip install "tulip-agents[bedrock]"; boto3 is imported lazily, so the four-package core install is unchanged for everyone not on AWS.

azure: names a deployment, not a model id, and reuses the openai extra — Azure's api-key header, api-version and deployment-shaped URLs are handled by the SDK's Azure client, so there is no second implementation to drift. gemini: uses Google's own OpenAI-compatible endpoint, so it needs no additional client either.

Configuration that is not in the environment travels in model_kwargs — a per-agent key, or a host that is not the default:

Agent(
    model="openai-compatible:qwen3.6-35b",
    model_kwargs={"base_url": "http://gpu-1:8000/v1", "api_key": "unused"},
)

Anything else implements ModelProtocolcomplete · stream, ~50 lines — and registers with register_provider("myco", MyModel).

Model providers · OpenAI-compatible endpoints


Notebooks, workbench, deploy

examples/ has progressive notebooks, numbered in suggested reading order. Each defaults to the bundled mock model when no API key is present.

git clone https://github.com/tuliplabs-ai/tulip-agents.git
cd tulip-agents && pip install -e .

python examples/notebook_06_basic_agent.py           # your first agent
python examples/notebook_58_cognitive_router.py      # the cognitive router
python examples/notebook_69_research_workflow.py     # full research pipeline

The workbench is a browser playground for every pattern — two clicks to a running agent, no editor setup. For production, AgentServer is a drop-in FastAPI app (POST /invoke, POST /stream, GET/DELETE /threads/{id}, GET /health) and the repo ships a multi-stage Dockerfile.

from tulip.server import AgentServer

AgentServer(agent=my_agent, api_key=os.environ["API_KEY"]).run(host="0.0.0.0", port=8080)

Notebooks · Workbench · Deploy


Any domain, one contract

The same contracts run wherever an agent acts. One fully worked domain package ships today: tulip.security applies the grounded-evidence contract to red-teaming AI systems — every result is a grounded Evidence tagged against public weakness catalogues (MITRE ATLAS, OWASP LLM / Agentic Top 10), or an explicit Abstention.

import asyncio

from tulip.security import Target, red_team, is_finding


async def main():
    report = await red_team(
        Target.endpoint("https://support-bot.example/chat"), suite="owasp-asi"
    )
    print([f for f in report.findings if is_finding(f)])


asyncio.run(main())

Vendor-specific adapters (Splunk, CrowdStrike, Okta, Auth0, VirusTotal, Wiz, RunPod, Lambda) live in tulip-integrations; core ships offline reference adapters so the SDK runs standalone.


Repo layout

src/tulip/
├── control/        Admission gate — Action, admit/approve, ControlPolicy, AuditTrail
├── rogue/          The rogue-agent challenge (`python -m tulip.rogue`)
├── agent/          Agent runtime, config, Sequential / Parallel / Loop pipelines
├── core/           AgentState, Message, events, termination algebra, Send
├── loop/           ReAct nodes (Think, Execute, Reflect)
├── router/         Cognitive router — GoalFrame, ProtocolRegistry, PolicyGate, Compiler
├── reasoning/      Reflexion, Grounding, Causal, GSAR
├── multiagent/     Orchestrator, Swarm, Handoff, StateGraph, Functional
├── a2a/            Cross-process Agent-to-Agent protocol
├── deepagent/      create_deepagent + create_research_workflow + 6 node primitives
├── memory/         BaseCheckpointer + 8 backends
├── rag/            Embeddings + 5 vector stores + rerankers + retrievers
├── models/         Provider registry + OpenAI, Anthropic, Bedrock, Azure
├── tools/          @tool decorator, registry, builtins, executors
├── hooks/          Logging, telemetry, retry, guardrails, steering
├── observability/  EventBus, run_context, agent yield bridge, EV_* constants
├── skills/         AgentSkills.io filesystem-first capability disclosure
├── playbooks/      Declarative step plans + PlaybookEnforcer
├── providers/      Multi-modal: web search, web fetch, image, speech
├── security/       Grounded findings, red-team / assure, taxonomy tags
├── server/         FastAPI AgentServer with thread persistence
├── evaluation/     EvalCase + EvalRunner + EvalReport
└── integrations/   MCP (client + server)

The docs site lives in a sibling repo: tuliplabs-ai/docs, published at tulipagents.ai.


Contributing

git clone https://github.com/tuliplabs-ai/tulip-agents.git
cd tulip-agents && pip install -e ".[dev,sdk]"
hatch run check        # ruff + mypy
hatch run test         # unit tests across Python 3.11–3.14
pre-commit install

See CONTRIBUTING.md. Every PR runs format, lint, mypy, unit tests, DCO sign-off. Please consult the security guide for vulnerability disclosure.


Citing GSAR

Paper: GSAR: Typed Grounding for Hallucination Detection and Recovery in Multi-Agent LLMs (PDF), 2026.

@article{gsar2026,
  title   = {GSAR: Typed Grounding for Hallucination Detection and Recovery in Multi-Agent LLMs},
  journal = {arXiv preprint arXiv:2604.23366},
  year    = {2026},
  url     = {https://arxiv.org/abs/2604.23366},
}

License

Copyright 2026 Tulip Labs.

Released under the Apache License, Version 2.0 — see LICENSE and NOTICE.

Tulip began as a fork of an earlier project released under the Universal Permissive License v1.0 (UPL-1.0); those original portions remain available under the UPL-1.0, while all new contributions are licensed under Apache-2.0. See NOTICE for 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_agents-2.12.3.tar.gz (1.3 MB view details)

Uploaded Source

Built Distribution

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

tulip_agents-2.12.3-py3-none-any.whl (741.6 kB view details)

Uploaded Python 3

File details

Details for the file tulip_agents-2.12.3.tar.gz.

File metadata

  • Download URL: tulip_agents-2.12.3.tar.gz
  • Upload date:
  • Size: 1.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tulip_agents-2.12.3.tar.gz
Algorithm Hash digest
SHA256 5358f7e28c90bd2dc25fd46ab2bbcefca683991ea4441108c4517bd65f6e7ba6
MD5 8e53891842db09d01649944ef7317264
BLAKE2b-256 a0eafd48353a6006e044d9aac093c4d78f486d0db79911bf76d6505acb007e99

See more details on using hashes here.

Provenance

The following attestation bundles were made for tulip_agents-2.12.3.tar.gz:

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

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_agents-2.12.3-py3-none-any.whl.

File metadata

  • Download URL: tulip_agents-2.12.3-py3-none-any.whl
  • Upload date:
  • Size: 741.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tulip_agents-2.12.3-py3-none-any.whl
Algorithm Hash digest
SHA256 5c7bfd69b21603e12856d81870fec7edcea7004c3bf05cd1002f64eb5700ff17
MD5 b3c694d082fd57a473c31ff9fe3841ef
BLAKE2b-256 0f9db4bd761499a4ee9634e66e3cb98be6c1245707582a6558b4dec825cc3b31

See more details on using hashes here.

Provenance

The following attestation bundles were made for tulip_agents-2.12.3-py3-none-any.whl:

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

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

Release history Release notifications | RSS feed

This release

2.12.3 This release

2 files

2.12.2

2 files

2.12.1

2 files

2.12.0

2 files

2.11.1

2 files

2.11.0

2 files

2.10.0

2 files

2.9.0

2 files

2.8.0

2 files

2.7.0

2 files

2.6.0

2 files

2.5.1

2 files

2.5.0

2 files

2.4.0

2 files

2.3.0

2 files

2.2.0

2 files

2.1.3

2 files

2.1.2

2 files

2.1.1

2 files

2.1.0

2 files

2.0.0

2 files

1.1.0

2 files

1.0.0

2 files

Supported by

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