Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 90 additions & 7 deletions how-to/adding-cycles-to-an-existing-application.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,12 @@ Pick the highest-value call path to wrap first. Good candidates:
- The call that runs most frequently
- The call most likely to loop or retry

### Wrapping an existing function (Python)
### Wrapping an existing function

**Before:**

```python
::: code-group
```python [Python]
def generate_summary(document: str) -> str:
response = openai.chat.completions.create(
model="gpt-4o",
Expand All @@ -96,10 +97,22 @@ def generate_summary(document: str) -> str:
)
return response.choices[0].message.content
```
```typescript [TypeScript]
async function generateSummary(document: string): Promise<string> {
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: `Summarize: ${document}` }],
max_tokens: 2000,
});
return response.choices[0].message.content!;
}
```
:::

**After:**

```python
::: code-group
```python [Python]
from runcycles import cycles

@cycles(
Expand All @@ -115,14 +128,35 @@ def generate_summary(document: str) -> str:
)
return response.choices[0].message.content
```
```typescript [TypeScript]
import { withCycles } from "runcycles";

const generateSummary = withCycles(
{
estimate: (document: string) => Math.ceil(document.length / 4 * 250 + 2000 * 1000) * 1.2,
actionKind: "llm.completion",
actionName: "openai:gpt-4o",
},
async (document: string) => {
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: `Summarize: ${document}` }],
max_tokens: 2000,
});
return response.choices[0].message.content!;
},
);
```
:::

The only change is adding the `@cycles` decorator. Your business logic stays exactly the same.
The only change is adding the `@cycles` decorator (Python) or `withCycles` wrapper (TypeScript). Your business logic stays exactly the same.

### Handling budget denial

Your existing error handling needs one new branch — what to do when budget is denied:

```python
::: code-group
```python [Python]
from runcycles import BudgetExceededError

try:
Expand All @@ -135,14 +169,32 @@ except BudgetExceededError:
# Option C: Queue for later
queue_for_retry(document)
```
```typescript [TypeScript]
import { BudgetExceededError } from "runcycles";

try {
const result = await generateSummary(document);
} catch (err) {
if (err instanceof BudgetExceededError) {
// Option A: Return a graceful fallback
result = "Summary unavailable — budget limit reached.";
// Option B: Use a cheaper model
result = await generateSummaryCheap(document);
// Option C: Queue for later
queueForRetry(document);
}
}
```
:::

See [Degradation Paths](/how-to/how-to-think-about-degradation-paths-in-cycles-deny-downgrade-disable-or-defer) for a full treatment of fallback strategies.

## Stage 3: Expand coverage

Once the first call path is working, wrap additional calls. Use a consistent pattern:

```python
::: code-group
```python [Python]
@cycles(estimate=500000, action_kind="llm.completion", action_name="openai:gpt-4o-mini")
def classify_intent(text: str) -> str:
...
Expand All @@ -155,14 +207,32 @@ def generate_response(context: str, intent: str) -> str:
def search_web(query: str) -> list:
...
```
```typescript [TypeScript]
const classifyIntent = withCycles(
{ estimate: 500000, actionKind: "llm.completion", actionName: "openai:gpt-4o-mini" },
async (text: string) => { ... },
);

const generateResponse = withCycles(
{ estimate: 3000000, actionKind: "llm.completion", actionName: "openai:gpt-4o" },
async (context: string, intent: string) => { ... },
);

const searchWeb = withCycles(
{ estimate: 100000, actionKind: "tool.call", actionName: "web-search" },
async (query: string) => { ... },
);
```
:::

Each wrapped function reserves independently. If the agent calls all three in sequence, the total budget consumed is the sum of actual usage — and each call is individually authorized before it runs.

## Stage 4: Switch to live enforcement

Once you're confident in your budget allocations (from shadow mode data), remove `dry_run=True`:

```python
::: code-group
```python [Python]
# Remove dry_run to enable enforcement
@cycles(
estimate=2000000,
Expand All @@ -173,6 +243,19 @@ Once you're confident in your budget allocations (from shadow mode data), remove
def generate_summary(document: str) -> str:
...
```
```typescript [TypeScript]
// Remove dryRun to enable enforcement
const generateSummary = withCycles(
{
estimate: 2000000,
actionKind: "llm.completion",
actionName: "openai:gpt-4o",
// dryRun: true, ← remove this line
},
async (document: string) => { ... },
);
```
:::

## Tips for existing applications

Expand Down
12 changes: 6 additions & 6 deletions quickstart/deploying-the-full-cycles-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ services:
timeout: 3s
retries: 5
cycles-admin:
image: ghcr.io/runcycles/cycles-server-admin:latest
image: ghcr.io/runcycles/cycles-server-admin:0.1.23.3
ports: ["7979:7979"]
environment:
REDIS_HOST: redis
Expand All @@ -51,7 +51,7 @@ services:
depends_on:
redis: { condition: service_healthy }
cycles-server:
image: ghcr.io/runcycles/cycles-server:latest
image: ghcr.io/runcycles/cycles-server:0.1.23.3
ports: ["7878:7878"]
environment:
REDIS_HOST: redis
Expand Down Expand Up @@ -196,7 +196,7 @@ services:
retries: 5

cycles-admin:
image: ghcr.io/runcycles/cycles-server-admin:latest
image: ghcr.io/runcycles/cycles-server-admin:0.1.23.3
ports:
- "7979:7979"
environment:
Expand All @@ -209,7 +209,7 @@ services:
condition: service_healthy

cycles-server:
image: ghcr.io/runcycles/cycles-server:latest
image: ghcr.io/runcycles/cycles-server:0.1.23.3
ports:
- "7878:7878"
environment:
Expand All @@ -230,8 +230,8 @@ Start the stack:
docker compose up -d
```

::: tip Pinning versions
Replace `:latest` with a specific version tag (e.g., `:0.1.23.3`) for reproducible deployments.
::: tip Version pinning
The examples above pin version `0.1.23.3`. Check [GitHub releases](https://github.com/runcycles/cycles-server/releases) for newer versions.
:::

Verify all services are healthy:
Expand Down
116 changes: 114 additions & 2 deletions quickstart/end-to-end-tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,51 @@ Run the [Runaway Agent Demo](https://github.com/runcycles/cycles-runaway-demo)

- **Docker** and **Docker Compose v2+**
- **Python 3.10+** or **Node.js 20+** (for the application step)
- An **OpenAI API key** (for the final step — you can skip this and use a mock if preferred)
- An **OpenAI API key** (for the final step — or use the mock tabs below if you don't have one)

## Quick code preview

Want to see what Cycles integration looks like before setting up the stack? Here is the complete pattern:

::: code-group
```python [Python]
from runcycles import CyclesClient, CyclesConfig, cycles, set_default_client

client = CyclesClient(CyclesConfig(
base_url="http://localhost:7878", # Cycles server
api_key="cyc_live_...", # from the admin API
tenant="my-app",
))
set_default_client(client)

@cycles(estimate=2000000, action_kind="llm.completion", action_name="openai:gpt-4o-mini")
def ask(prompt: str) -> str:
return call_your_llm(prompt) # any LLM provider

result = ask("Hello") # Budget reserved → LLM called → cost committed
```
```typescript [TypeScript]
import { CyclesClient, CyclesConfig, withCycles, setDefaultClient } from "runcycles";

const client = new CyclesClient(new CyclesConfig({
baseUrl: "http://localhost:7878",
apiKey: "cyc_live_...",
tenant: "my-app",
}));
setDefaultClient(client);

const ask = withCycles(
{ estimate: 2000000, actionKind: "llm.completion", actionName: "openai:gpt-4o-mini" },
async (prompt: string) => callYourLlm(prompt),
);

const result = await ask("Hello"); // Budget reserved → LLM called → cost committed
```
:::

::: info
This code requires a running Cycles server. The tutorial below walks you through setting one up with Docker in about 2 minutes. If you just want to see the demo without any setup, try the [Runaway Agent Demo](https://github.com/runcycles/cycles-runaway-demo) instead.
:::

## Step 1: Start the Cycles stack

Expand Down Expand Up @@ -213,17 +257,85 @@ const ask = withCycles(
},
);

const result = await ask("What is budget governance for AI agents? Reply in one sentence.");
console.log("Response:", result);
```
```python [Python (mock)]
# Install: pip install runcycles
# Save as app_mock.py — no OpenAI key needed

import os
from runcycles import CyclesClient, CyclesConfig, cycles, set_default_client

# Configure Cycles
cycles_client = CyclesClient(CyclesConfig(
base_url="http://localhost:7878",
api_key=os.environ["CYCLES_API_KEY"],
tenant="my-app",
))
set_default_client(cycles_client)

@cycles(
estimate=2000000, # Reserve $0.02 per call
action_kind="llm.completion",
action_name="mock:gpt-4o-mini",
)
def ask(prompt: str) -> str:
# Simulated LLM response — no API key required
return f"[Mock response to: {prompt[:50]}]"

# Run it
try:
result = ask("What is budget governance for AI agents? Reply in one sentence.")
print(f"Response: {result}")
except Exception as e:
print(f"Error: {e}")
```
```typescript [TypeScript (mock)]
// Install: npm init -y && npm install runcycles
// Save as app_mock.ts — no OpenAI key needed

import { CyclesClient, CyclesConfig, withCycles, setDefaultClient } from "runcycles";

const cyclesClient = new CyclesClient(new CyclesConfig({
baseUrl: "http://localhost:7878",
apiKey: process.env.CYCLES_API_KEY!,
tenant: "my-app",
}));
setDefaultClient(cyclesClient);

const ask = withCycles(
{
estimate: 2000000,
actionKind: "llm.completion",
actionName: "mock:gpt-4o-mini",
},
async (prompt: string) => {
// Simulated LLM response — no API key required
return `[Mock response to: ${prompt.slice(0, 50)}]`;
},
);

const result = await ask("What is budget governance for AI agents? Reply in one sentence.");
console.log("Response:", result);
```
:::

::: tip No OpenAI key?
The **mock tabs** replace the OpenAI call with a stub that returns a fixed string. The Cycles budget lifecycle (reserve → commit → balance deduction) works exactly the same — you just skip the LLM cost.
:::

Run it:

```bash
export CYCLES_API_KEY="cyc_live_..." # your key from Step 3
export OPENAI_API_KEY="sk-..." # your OpenAI key

# With OpenAI:
export OPENAI_API_KEY="sk-..."
python app.py # or: npx tsx app.ts

# Without OpenAI (mock):
python app_mock.py # or: npx tsx app_mock.ts
```

## Step 7: Watch the budget decrease
Expand Down
1 change: 1 addition & 0 deletions quickstart/what-is-cycles.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ Your application talks to the **Cycles Server** for runtime budget checks. The *
## Next steps

- [End-to-End Tutorial](/quickstart/end-to-end-tutorial) — zero to a working budget-guarded app in 10 minutes
- [Choose a First Rollout](/quickstart/how-to-choose-a-first-cycles-rollout-tenant-budgets-run-budgets-or-model-call-guardrails) — decide your adoption strategy
- [Deploy the Full Stack](/quickstart/deploying-the-full-cycles-stack) — set up the Cycles infrastructure
- [Python Quickstart](/quickstart/getting-started-with-the-python-client) — add Cycles to a Python app
- [TypeScript Quickstart](/quickstart/getting-started-with-the-typescript-client) — add Cycles to a TypeScript app
Expand Down
Loading