> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cycls.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agents

> A chat product in one decorator: managed LLM loop, tools, files, history and a web interface.

An agent is an async generator that receives a `context` and yields events. Cycls
wraps it in a web application, a streaming protocol, per-user storage and a
managed model loop.

```python agent.py theme={null}
import cycls

llm = (
    cycls.LLM()
    .model("anthropic/claude-sonnet-4-6")
    .system("You are a helpful assistant.")
    .allowed_tools(["Bash", "Editor", "WebSearch"])
)


@cycls.agent(
    image=cycls.Image().copy(".providers.env", ".env"),
    web=cycls.Web().auth(cycls.Clerk()).title("My Agent"),
    volumes={"/workspace": cycls.Volume("my-agent")},
)
async def my_agent(context):
    async for ev in llm.run(context=context):
        yield ev
```

```bash theme={null}
cycls run my_agent.py      # localhost:8080
cycls deploy my_agent.py   # https://my-agent.cycls.ai
```

## The decorator

```python theme={null}
@cycls.agent(name=None, image=None, web=None, volumes=None, memory="1Gi")
```

| Argument  | Meaning                                                                           |
| --------- | --------------------------------------------------------------------------------- |
| `name`    | deployment name and subdomain. Defaults to the function name                      |
| `image`   | [`cycls.Image`](/build/images) for packages and bundled files                     |
| `web`     | [`cycls.Web`](/web/interface) for interface, auth and branding                    |
| `volumes` | mount paths to [`cycls.Volume`](/build/volumes). A `/workspace` entry is required |
| `memory`  | container memory, for example `"1Gi"`, `"4Gi"`                                    |

<Warning>
  Agents keep chats, files and credentials under `/workspace`, so the decorator
  raises if no volume is mounted there. Locally, `cycls run` ignores volumes and
  your code sees the local filesystem.
</Warning>

## What the managed loop does

`llm.run(context=context)` is the default loop. Yielding its events through your
body is all a normal agent needs.

<CardGroup cols={2}>
  <Card title="Model calls and streaming" icon="bolt">
    Text, reasoning and tool-call deltas from Anthropic natively and from any
    OpenAI-compatible endpoint.
  </Card>

  <Card title="Tool execution" icon="wrench">
    Built-in tools and your handlers, run in parallel where the provider sends a
    batch, with results fed back to the model.
  </Card>

  <Card title="Retries and recovery" icon="arrows-rotate">
    Transient provider failures are retried with backoff. Context overflow
    triggers one compaction and a replay.
  </Card>

  <Card title="Compaction" icon="compress">
    When the window fills, older turns are summarized behind an append-only
    marker so the conversation continues.
  </Card>

  <Card title="Persistence" icon="floppy-disk">
    The user's turn is written to disk before the model is called, so a dropped
    connection never loses it.
  </Card>

  <Card title="Cost accounting" icon="receipt">
    Every turn logs tokens and cost when `.price()` is set, queryable with
    `cycls cost` and `cycls sql`.
  </Card>
</CardGroup>

## A turn, end to end

```mermaid theme={null}
sequenceDiagram
    participant U as Browser
    participant A as Agent body
    participant L as Managed loop
    participant M as Model
    participant T as Tools
    U->>A: POST / with messages
    A->>L: llm.run(context=context)
    L->>L: load session, ingest attachments, build system prompt
    L->>M: stream request
    M-->>L: thinking, text, tool calls
    L->>T: run tool batch
    T-->>L: results
    L->>M: continue with results
    M-->>L: final text
    L-->>A: events
    A-->>U: SSE stream
```

## Yielding your own events

The body is ordinary Python, so you can emit anything before, during or after the
loop. Events are plain dicts, and strings are markdown text.

```python theme={null}
@cycls.agent(web=web, volumes={"/workspace": chats})
async def my_agent(context):
    if context.user.plan == "u:free_user" and await over_quota(context):
        yield {"type": "callout", "callout": "Free limit reached.", "style": "warning"}
        yield {"type": "ui", "action": "open_plan_modal"}
        return

    yield {"type": "status", "status": "Thinking about it"}

    async for ev in llm.run(context=context):
        yield ev

    yield "\n\n_Answered by My Agent._"
```

See [Streaming components](/agents/streaming) for every event type.

## Watching the loop

Events are dicts, so a plain check is enough to react without changing behavior.

```python theme={null}
async for ev in llm.run(context=context):
    if isinstance(ev, dict) and ev.get("type") == "callout" and ev.get("style") == "error":
        alert_ops(ev["callout"])
    yield ev
```

<Note>
  `cycls.to_ui(ev)` still appears in older examples. It is now an identity function
  kept for backwards compatibility, so `yield ev` and `yield cycls.to_ui(ev)` do
  the same thing.
</Note>

## Adding HTTP routes

An agent is also a web service. Use `.server` for webhooks, health checks and
OAuth callbacks, and `Depends(my_agent.auth)` to protect a route with the same
identity the chat endpoint uses.

```python theme={null}
from fastapi import Depends


@my_agent.server.api_route("/webhook", methods=["POST"])
async def 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 {"id": user.id, "plan": user.plan}
```

## Next steps

<CardGroup cols={2}>
  <Card title="Models" icon="brain" href="/agents/models">
    Providers, reasoning, budgets and pricing.
  </Card>

  <Card title="Tools" icon="wrench" href="/agents/tools">
    Built-in tools and your own handlers.
  </Card>

  <Card title="Context" icon="user" href="/agents/context">
    Messages, users, workspaces and per-request switches.
  </Card>

  <Card title="Custom loops" icon="code-branch" href="/agents/custom-loop">
    Replace the default loop while keeping the kit.
  </Card>
</CardGroup>
