Core install; provider required only for model calls
Controls apply only where configured and wired
OpenAI, Anthropic, Azure OpenAI, Bedrock, OpenRouter, and Together.ai model access
Supported
Live; provider extra and credential required
Azure and Bedrock are native transports; OpenRouter and Together.ai use the OpenAI-compatible transport; model capabilities vary
Checkpointer and vector-store adapters
Supported interfaces
Live infrastructure for non-memory backends; matching extra/config required
Operate and test the backing service yourself
SecurityContext reference adapters
Offline simulation
No credentials
Vendor write templates return simulated receipts unless replaced and verified
Published research and evaluation artifacts
Research-only
See each research page
Some source datasets and evaluated model artifacts are not publicly available
This build targets tulip-agents 2.28.0 on Python 3.11–3.14. See the
compatibility policy for label definitions and upgrade
guidance.
Distinctive to the SDK
The control runtime — let an agent act, on your terms. A
side-effecting action (a refund, a production deploy, a GDPR deletion)
runs only after it clears a ControlPolicy you write: approve()
weighs it, admit() runs it only on an allow, or on a hold a named
person approves (approved_by=), and a supplied
AuditTrail records each decision (each entry is chained to
the one before it — editing any record breaks verify()). Policy →
approval → admission → audit, enforced in code, not convention.
Eight native multi-agent shapes — Composition
(Sequential / Parallel / Loop), Orchestrator + Specialists, Swarm,
Handoff, StateGraph, and cross-process A2A — plus the Functional API
(@task / @entrypoint) and DeepAgent (a research factory built on
top).
Use them directly, or reach for them from the loop as tools.
In-process observability — opt-in EventBus with agent yield
bridge. One run_context() streams 70+ canonical events from every
layer (agent, multi-agent, RAG, memory, A2A). Zero allocations
when unused.
Reasoning loop nodes — Reflexion, Grounding, Causal as first-class
Think → Execute → Reflect → Think nodes, not bolted-on libraries.
GSAR — typed-grounding safety layer from
arXiv:2604.23366 (2026):
four-way claim partition (grounded / ungrounded / contradicted /
complementary) + tiered replanning decisions. A claim graded by
ground_finding() becomes a typed Evidence only above the threshold,
else it abstains, and
verify() challenges it before it drives an action.
Termination algebra — MaxIterations(10) | TextMention("DONE") & ConfidenceMet(0.9) is real Python (__or__ / __and__ overloads). Greppable, unit-testable, serialisable.
Idempotent tools — @tool(idempotent=True) dedupes identical
(name, args) calls inside the documented run/checkpoint scope. External
systems still need stable idempotency keys for crash recovery.
OpenAI, Anthropic, Azure OpenAI, Bedrock, and compatible providers —
OpenAI, Anthropic, Azure OpenAI and Bedrock as native transports, plus
hosted and self-hosted OpenAI-compatible endpoints (OpenRouter,
Together.ai and more) auto-routed by model prefix. One
get_model() call, any provider.
Every pattern maps to a real workflow across domains — payments, infra,
support, data, security. The shape is the discipline: who runs in
parallel and who hands off. No shape gates whether a refund clears, a host
is isolated, or a record is deleted — route that side effect through
admit().
Shape
Maps to
Surface
Composition
Payments: sequential fraud-check → risk-score → settle; parallel enrichment fan-out over a transaction's signals
# Orchestrator over fraud-check, risk-score and settlement: the desk's model# picks which run, the picked ones run in parallel, then the desk correlates# and summarizes. Specialists never see each other's work, and their tool# calls are not executed — send the refund itself through admit().fromtulip.modelsimportget_modelfromtulip.multiagentimportOrchestrator,Specialistmodel=get_model("openai:gpt-4o-mini")settlement=Specialist(name="settlement",specialist_type="settlement",description="Recommends how to settle a dispute.",system_prompt="Recommend a settlement, citing the evidence in the task.",model=model,)desk=Orchestrator(model=model)desk.register_specialists([fraud_check,risk_score,settlement])result=awaitdesk.execute("Recommend how to resolve the duplicate charge on order 8842.")
The most fully built domain package, and the proof the chain holds under
pressure: point an agent at an AI or at infrastructure, and every finding comes
back grounded or abstained, ready to be verified, gated, and audited. The same
chain — grounding, gating, audit — applies to any domain: a claim graded by
ground_finding() becomes evidence only when it clears the GSAR threshold, a
side effect routed through admit() runs only on an allow decision or on a hold
a named person approves, and admit() records every decision it makes (allowed,
held or denied) on the trail you pass. What it cannot do is gate a side effect
that never passes through it, or classify your actions for you — an action left
labelled environment="staging" never matches a policy that holds production,
so see Guarantees and boundaries.
Feature
What it does
Surface
Grounded findings
ground_finding returns a typed Evidence only when the claim clears the GSAR threshold — else an Abstention. Build findings through it, not by constructing Evidence directly.
# A finding only exists above the GSAR bar — else it abstains. ground_finding never# returns an ungrounded Evidence.fromtulip.controlimportground_finding,Severity,is_findingresult=ground_finding(title="Expired TLS certificate on 192.0.2.10:443",description="Certificate expired 3 days ago; clients see TLS errors.",severity=Severity.HIGH,asset="192.0.2.10:443",remediation="Renew and redeploy the certificate.",partition=partition,# GSAR claim partition from tool evidence)ifis_finding(result):print("SHIPPED",result.title,result.gsar_score)else:print("ABSTAINED",result.decision,"—",result.reason)
# The action chain: investigate → verify → policy → admission gate.# isolate_host fires only if the chain clears; production → hold (require_human).fromtulip.controlimportActionfromtulip_securityimportSecurityContext,verifyctx=SecurityContext()verdict=awaitverify(finding)awaitctx.actions.execute(Action(name="isolate_host",asset="WS-0142",environment="production"),lambda:ctx.endpoint.isolate("WS-0142"),# side effect, gatedfinding=finding,verdict=verdict,)# raises AdmissionError unless the policy allows (here production → hold); call admit(..., trail=trail, approved_by=...) directly to record it or approve the hold