Quickstart¶
A working Tulip SOC agent in five minutes.
1. Install¶
This installs the SDK plus the OpenAI provider. For other providers add the corresponding extra:
pip install "tulip-agents[openai]" # OpenAI directly
pip install "tulip-agents[anthropic]" # Anthropic directly
pip install "tulip-agents[sdk]" # everything
2. Configure your provider¶
Set the API key for whichever provider you're using:
For OpenAI-compatible gateways or local servers, point OpenAIModel at
the model's base_url instead. See
Models for the per-provider matrix. A provider
key is required for real model output; the example notebooks can also run
offline against the MockModel bundled with the examples.
3. Your first agent¶
Save this as soc_agent.py:
from tulip.agent import Agent
from tulip.tools.decorator import tool
@tool
def scan_endpoint(ip: str, port: int) -> dict:
"""Scan an endpoint for cert expiry and known CVEs."""
return {"ip": ip, "port": port, "cert_not_after": "2026-05-30",
"cves": ["CVE-2024-3094"]}
@tool
def check_domain_reputation(domain: str) -> dict:
"""Look up a domain in the threat-intel feeds."""
return {"domain": domain, "verdict": "malicious", "sources": 4}
agent = Agent(
model="openai:gpt-4o",
tools=[scan_endpoint, check_domain_reputation],
system_prompt="You are a SOC analyst triaging alerts. "
"Cite the evidence behind every verdict.",
)
result = agent.run_sync(
"Scan 192.0.2.10:443 for cert expiry and check its reputation "
"in our threat feeds."
)
print(result.message)
Run:
You should see something like:
192.0.2.10:443 serves an expired cert (not_after 2026-05-30) and is
vulnerable to CVE-2024-3094. Evidence: scan_endpoint + threat feeds
flag the host malicious across 4 sources. Verdict: HIGH, isolate.
3.5 Gate the action¶
Finding the problem is the easy half. The moment an agent goes from
advising to acting — isolating that host, refunding that customer,
disabling that account — you need a gate it cannot talk its way past.
That's the whole point of Tulip: wrap the action in admit() and it runs
only after it clears policy, holds for a human when the blast radius
warrants it, and lands on a tamper-evident audit trail either way.
from tulip.control import (
Action, admit, ControlPolicy, AuditTrail, AdmissionError)
policy = ControlPolicy() # conservative defaults: production → human
trail = AuditTrail() # hash-chained, replayable, tamper-evident
# The triage above said "isolate 192.0.2.10". Don't just do it — admit it.
risky = Action(
name="isolate_host", asset="192.0.2.10",
blast_radius=1, kind="containment", environment="production")
async def isolate():
... # your real containment call (CrowdStrike, firewall, etc.)
try:
await admit(risky, isolate, policy=policy, trail=trail)
except AdmissionError as e:
print(e.decision.outcome) # -> "require_human"; isolate did NOT run
# Either way it's on the record:
print(trail.verify()) # True — chain intact
print(trail.export_jsonl()) # SIEM-ready, one event per line
That admit() call is the difference between a library that suggests
and a runtime that enforces. See Why Tulip for the
comparison, and the Admission gate concept for
the full policy surface.
4. Stream the events¶
For UIs and real-time logging, switch to async and consume the typed event stream:
import asyncio
from tulip.core.events import (
ThinkEvent, ToolStartEvent, ToolCompleteEvent, TerminateEvent,
)
async def main():
async for event in agent.run("Scan 192.0.2.10:443 and triage it."):
match event:
case ThinkEvent(reasoning=r) if r:
print(f"💭 {r}")
case ToolStartEvent(tool_name=n, arguments=a):
print(f"🔧 {n}({a})")
case ToolCompleteEvent(result=r):
print(f" ↳ {r}")
case TerminateEvent(final_message=m):
print(f"\n✅ {m}")
asyncio.run(main())
See Streaming for the full event taxonomy.
5. Persist conversations across restarts¶
For real applications you'll want state to survive a restart. Wire a
checkpointer and a thread_id:
from tulip.memory.backends.file import FileCheckpointer
agent = Agent(
model="openai:gpt-4o",
tools=[...],
system_prompt="...",
checkpointer=FileCheckpointer(base_dir="./threads"),
)
# Day 1
agent.run_sync("Open the investigation for alert A-42.", thread_id="case-4821")
# Day 2 — same thread_id, the investigation continues
agent.run_sync("What did we establish so far?", thread_id="case-4821")
For vendor-neutral durability, swap to S3Backend(bucket=..., prefix=...).
See Conversation Management.
6. Make it production-grade¶
Add idempotency to side-effecting tools, Reflexion to catch wrong premises, and termination algebra to stop when the work is done:
from tulip.memory.backends import S3Backend
from tulip.core.termination import (
MaxIterations, ToolCalled, ConfidenceMet,
)
@tool(idempotent=True)
def isolate_host(host_id: str, incident_id: str) -> dict:
return edr.isolate(host_id, incident_id)
agent = Agent(
model="openai:gpt-4o",
tools=[query_siem, isolate_host],
system_prompt="...",
reflexion=True,
checkpointer=S3Backend(bucket="tulip-threads", prefix="..."),
termination=(
ToolCalled("isolate_host") & ConfidenceMet(0.9)
) | MaxIterations(8),
)
Each piece in detail:
@tool(idempotent=True)→ Idempotencyreflexion=True→ Reasoningcheckpointer=...→ Checkpointerstermination=...→ Termination
7. Multi-agent¶
When one agent isn't enough — pick the coordination shape that fits the problem:
| Shape | When |
|---|---|
| Composition | linear chain, fan-out + merge |
| Orchestrator + Specialists | one router, parallel experts |
| Swarm | open-ended research, peer-to-peer |
| Handoff | escalation desks |
| StateGraph | review-loops, retry-until-confidence |
| Functional API | map/reduce over agents |
| A2A | cross-process meshes |
8. Deploy¶
AgentServer is a drop-in FastAPI app:
from tulip.server import AgentServer
server = AgentServer(agent=agent, api_key="change-me")
server.run(host="0.0.0.0", port=8080)
POST /invoke, POST /stream, GET /threads/{id}, GET /health.
Binding to a non-loopback host requires an api_key (or
allow_unauthenticated=True); every route except /health then
expects that bearer token. Deploys anywhere FastAPI runs — see
Deploy.
Where to next¶
- Read deeper. Agent Loop is the architectural reference for how all of this fits together.
- Browse examples. Progressive notebooks at
examples/. Each is a single runnable file that adds one idea on top of the previous. - Steer it. Hooks give you logging, telemetry, retry, guardrails, and steering as one-line additions.