Skip to main content

Distribute Intelligence

Project description

Distribute Intelligence

Website | Docs | Tutorial

cycls Python package on PyPi Tests Cycls newsletter Cycls Twitter


Cycls

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)

Distribute Intelligence

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 msg
cycls deploy my_agent.py   # live at https://my-agent.cycls.ai

Installation

pip install cycls

Requires Docker. See the full tutorial for a comprehensive guide.

The Primitives

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.

Running

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.ai

Or via the CLI (recommended):

cycls run my_agent.py       # local Docker + hot-reload
cycls deploy my_agent.py    # production

Get an API key at cycls.com.

Authentication

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, ...)
    ...

Analytics & Billing

web = (
    cycls.Web()
    .auth(cycls.Clerk())
    .analytics(True)        # usage metrics on the Cycls dashboard
    .plan("cycls_pass")     # monetize via Cycls Pass subscriptions
    .title("My Agent")
)

Custom Tools

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"![image]({args['src']})"}


llm = (
    cycls.LLM()
    .model("anthropic/claude-sonnet-4-6")
    .tools(TOOLS)
    .on("render_image", render_image)
)

Multi-provider LLM

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 inference

Thinking/reasoning events, tool calls, and streaming are unified across providers.

Streaming Components

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

Thinking Bubbles

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.

Context Object

@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.workspace     # Per-user persistent workspace Path

API Endpoints

Endpoint Format
POST /chat/cycls Cycls streaming protocol
POST /chat/completions OpenAI-compatible

HTTP Extension

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}

Declarative Infrastructure

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()
    ...

.pip(*packages) — Python packages

Install any packages from PyPI during container build.

cycls.Image().pip("openai", "pandas", "numpy", "transformers")

.apt(*packages) — System packages

Install apt-get dependencies. Need ffmpeg? ImageMagick? Declare it.

cycls.Image().apt("ffmpeg", "imagemagick", "libpq-dev")

.copy(src, dst=None) — Bundle files

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
    ...

.run(command) — Build-time shell commands

cycls.Image().run("pip install --upgrade pip").run("apt-get clean")

.rebuild() — Force Docker cache bust

image = cycls.Image().pip("numpy").rebuild()   # skip Docker cache

Public static files via cycls.Web

Static 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.


What You Get

  • 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 + SDKcycls 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.

Learn More

  • Tutorial — comprehensive guide from basics to advanced
  • Examples — working code samples

License

MIT

Project details


Release history Release notifications | RSS feed

Download files

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

Source Distribution

cycls-0.0.2.123.tar.gz (2.9 MB view details)

Uploaded Source

Built Distribution

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

cycls-0.0.2.123-py3-none-any.whl (3.2 MB view details)

Uploaded Python 3

File details

Details for the file cycls-0.0.2.123.tar.gz.

File metadata

  • Download URL: cycls-0.0.2.123.tar.gz
  • Upload date:
  • Size: 2.9 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.25 {"installer":{"name":"uv","version":"0.9.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for cycls-0.0.2.123.tar.gz
Algorithm Hash digest
SHA256 5bbf2bb11d85d65616c3e1965e956841d1dab674cb3f89da95118f87aabf381f
MD5 f8445167fce7021e90061fba4e17613e
BLAKE2b-256 c82414ae5b0ec1fc5d17a2afa30f862af6149794331292af3befb600ca9a4c27

See more details on using hashes here.

File details

Details for the file cycls-0.0.2.123-py3-none-any.whl.

File metadata

  • Download URL: cycls-0.0.2.123-py3-none-any.whl
  • Upload date:
  • Size: 3.2 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.25 {"installer":{"name":"uv","version":"0.9.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for cycls-0.0.2.123-py3-none-any.whl
Algorithm Hash digest
SHA256 51a0efb0db11d12111fe2ab0a5d9969c4224703da92199b9ba29ed398ba006e8
MD5 7f1a40f1544e8e7768a5dde60f52a0d1
BLAKE2b-256 be112bd5eee1b5b47007c6f1ab9fa471e18e85d0d185a5816b5b48d30d60dadb

See more details on using hashes here.

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