The deep-stack AI SDK for Python. Every layer of an AI agent — runtime, interface, intelligence, state — as a composable Python primitive, in one file, deployed with one command.
Agent extends App (chat product + managed LLM loop)
└── App extends Function (blocking ASGI service)
└── Function (Docker containerization)
Write an agent. Three primitives compose it. Deploy it with one command.
import cycls
image = cycls.Image().copy(".env")
web = (
cycls.Web()
.auth(cycls.Clerk())
.title("My Agent")
)
llm = (
cycls.LLM()
.model("anthropic/claude-sonnet-4-6")
.system("You are a helpful assistant.")
.allowed_tools(["Bash", "Editor", "WebSearch"])
)
@cycls.agent(image=image, web=web)
async def my_agent(context):
async for msg in llm.run(context=context):
yield msgcycls deploy my_agent.py # live at https://my-agent.cycls.aipip install cyclsRequires Docker. See the full tutorial for a comprehensive guide.
Four composable builders, three decorators, one CLI.
Primitives (declare once, reuse anywhere):
cycls.Image — container build config (pip, apt, copy, run commands)
cycls.Web — UI, auth, branding, billing, analytics
cycls.LLM — model, system prompt, tools, runtime config
cycls.Clerk — Clerk JWT auth provider (or cycls.JWT for generic OIDC)
Decorators (compose primitives into deployable units):
@cycls.function(image=) — non-blocking compute
@cycls.app(image=) — blocking ASGI service
@cycls.agent(image=, web=) — managed chat product
CLI:
cycls run file.py — local Docker with hot-reload
cycls deploy file.py — production deploy
cycls ls — list deployments
cycls logs <name> -f — tail logs
cycls rm <name> — delete a deployment
cycls init [name] — scaffold a starter agent
Every primitive is a fluent immutable builder. Every decorator accepts exactly those primitives, never grab-bag kwargs.
my_agent.local() # local Docker + hot-reload (localhost:8080)
my_agent.local(watch=False) # local Docker, no watch
my_agent.deploy() # production: https://my-agent.cycls.aiOr via the CLI (recommended):
cycls run my_agent.py # local Docker + hot-reload
cycls deploy my_agent.py # productionGet an API key at cycls.com.
Auth providers are first-class objects. cycls.Clerk() uses Cycls's hosted Clerk by default; cycls.JWT(...) covers any OIDC provider (Auth0, WorkOS, Supabase, Okta, Firebase).
# Cycls's default Clerk (dev/prod dual mode, auto-switches)
web = cycls.Web().auth(cycls.Clerk())
# Custom Clerk tenant
web = cycls.Web().auth(cycls.Clerk(
jwks_url="https://clerk.mycompany.com/.well-known/jwks.json",
))
# Generic OIDC (Auth0, WorkOS, etc)
web = cycls.Web().auth(cycls.JWT(
jwks_url="https://my-prod.auth0.com/.well-known/jwks.json",
dev_jwks_url="https://my-dev.auth0.com/.well-known/jwks.json",
))
@cycls.agent(web=web)
async def my_agent(context):
user = context.user # User(id, org_id, plan, features, ...)
...web = (
cycls.Web()
.auth(cycls.Clerk())
.analytics(True) # usage metrics on the Cycls dashboard
.cms("cycls.ai") # CMS entry → monetize via Cycls Pass subscriptions
.title("My Agent")
)Tools are bare JSON schemas. Handlers are plain async functions registered via .on(name, handler). Handler return values flow to both the UI stream and the LLM's tool_result.
TOOLS = [
{
"name": "render_image",
"description": "Display an image to the user.",
"inputSchema": {
"type": "object",
"properties": {"src": {"type": "string"}},
"required": ["src"],
},
}
]
async def render_image(args):
return {"type": "text", "text": f""}
llm = (
cycls.LLM()
.model("anthropic/claude-sonnet-4-6")
.tools(TOOLS)
.on("render_image", render_image)
)One adapter covers Anthropic natively and every OpenAI-compatible endpoint (OpenAI, Groq, vLLM, HUMAIN, self-hosted, ...) via provider/model strings:
cycls.LLM().model("anthropic/claude-sonnet-4-6") # Anthropic native
cycls.LLM().model("openai/gpt-5.4") # OpenAI
cycls.LLM().model("groq/llama-3.3-70b").base_url(...) # Groq or any OpenAI-compat
cycls.LLM().model("humain/jais").base_url(...) # sovereign inferenceThinking/reasoning events, tool calls, and streaming are unified across providers.
Yield structured objects from an agent body for rich streaming responses:
@cycls.agent(web=cycls.Web().auth(cycls.Clerk()))
async def demo(context):
yield {"type": "thinking", "thinking": "Analyzing the request..."}
yield "Here's what I found:\n\n"
yield {"type": "table", "headers": ["Name", "Status"]}
yield {"type": "table", "row": ["Server 1", "Online"]}
yield {"type": "table", "row": ["Server 2", "Offline"]}
yield {"type": "code", "code": "result = analyze(data)", "language": "python"}
yield {"type": "callout", "callout": "Analysis complete!", "style": "success"}| Component | Streaming |
|---|---|
{"type": "thinking", "thinking": "..."} |
Yes |
{"type": "code", "code": "...", "language": "..."} |
Yes |
{"type": "table", "headers": [...]} / {"type": "table", "row": [...]} |
Yes |
{"type": "status", "status": "..."} |
Yes |
{"type": "callout", "callout": "...", "style": "..."} |
Yes |
{"type": "image", "src": "..."} |
Yes |
The {"type": "thinking", ...} component renders as a collapsible thinking bubble. Consecutive thinking yields append to the same bubble until a different component type is yielded. Cycls automatically maps provider reasoning deltas (Claude extended thinking, OpenAI delta.reasoning) to this channel, so you get thinking bubbles without doing anything special.
@cycls.agent(web=cycls.Web().auth(cycls.Clerk()))
async def chat(context):
context.messages # [{"role": "user", "content": "..."}]
context.messages.raw # Full data including UI component parts
context.user # User(id, org_id, plan, features, ...) when auth is set
context.prod # True via .deploy(), False via .local() — gate billing/analytics
with context.workspace(): # Per-user persistent scope — enables cycls.Dict(...)
usage = cycls.Dict("usage")| Endpoint | Format |
|---|---|
POST /chat/cycls |
Cycls streaming protocol |
POST /chat/completions |
OpenAI-compatible |
Agents expose the underlying FastAPI surface via .server for webhooks, health checks, OAuth callbacks, and any custom routes:
@cycls.agent(web=cycls.Web().auth(cycls.Clerk()))
async def my_agent(context):
async for msg in llm.run(context=context):
yield msg
@my_agent.server.api_route("/webhook", methods=["POST"])
async def stripe_webhook(request):
payload = await request.json()
...
return {"ok": True}
@my_agent.server.api_route("/profile", methods=["GET"])
async def profile(user = Depends(my_agent.auth)):
return {"user_id": user.id}The cycls.Image primitive holds container build config. Every field is chainable; the resulting Image is passed to any decorator via image=.
image = (
cycls.Image()
.pip("openai", "pandas", "numpy", "transformers")
.apt("ffmpeg", "imagemagick", "libpq-dev")
.copy("./utils.py")
.copy("./models/", "app/models/")
.copy("/absolute/path/to/config.json")
.run("echo 'hello from build' > /app/build_marker.txt")
)
@cycls.function(image=image)
def my_func(x):
from utils import helper_function # bundled via .copy()
...Install any packages from PyPI during container build.
cycls.Image().pip("openai", "pandas", "numpy", "transformers")Install apt-get dependencies. Need ffmpeg? ImageMagick? Declare it.
cycls.Image().apt("ffmpeg", "imagemagick", "libpq-dev")Include local files and directories. Works with relative or absolute paths, single files or whole trees. dst defaults to src; pass both to relocate.
(
cycls.Image()
.copy("./utils.py") # same path
.copy("./models/", "app/models/") # src → dst
.copy("/home/user/configs/app.json") # absolute
)Import bundled modules in your function body:
@cycls.function(image=cycls.Image().copy("./utils.py"))
def my_func(x):
from utils import helper_function
...cycls.Image().run("pip install --upgrade pip").run("apt-get clean")image = cycls.Image().pip("numpy").rebuild() # skip Docker cacheStatic files served from /public (images, downloads, assets) live on the Web primitive:
web = cycls.Web().copy_public("./assets/logo.png", "./downloads/")Access them at https://your-app.cycls.ai/public/logo.png.
- One file — Primitives, code, and infrastructure together
- Three decorators —
@function,@app,@agent, each one strict and composable - Multi-LLM — Anthropic native + every OpenAI-compatible endpoint
- Managed loop — retries, compaction, sandbox, tool handlers, history, sessions
- CLI + SDK —
cycls run,cycls deploy, or programmatic.local()/.deploy() - No drift — what you see is what runs
No YAML. No Dockerfiles. No infrastructure repo. The code is the deployment.
- Tutorial — comprehensive guide from basics to advanced
- Sandbox security — how the Bash tool is isolated
- Examples — working code samples
MIT