---
title: Migrating from v0.11 to v0.12
description: Breaking changes, removed aliases, and new production capabilities in v0.12.0.
weight: 20
---

## What Changed and Why

v0.12.0 is the "Production Sandbox" release. The headline additions — `exec_bash`, packages auto-install, the async context manager, and sandbox propagation through `RLMLoop` — are all additive. You don't have to use any of them to upgrade.

But six things are breaking. Read this page before you upgrade.

## The Six Breaking Changes

### 1. Memory.recall() — `count=` and `top_k=` removed

In v0.11, `recall()` accepted `count=` and `top_k=` as aliases for `limit=`. Those aliases are gone.

**Before (v0.11):**

```python
memories = agent.memory.recall("user preferences", count=5)
memories = agent.memory.recall("user preferences", top_k=10)
```

**After (v0.12):**

```python
memories = agent.memory.recall("user preferences", limit=5)
memories = agent.memory.recall("user preferences", limit=10)
```

Find-and-replace `count=` and `top_k=` with `limit=` in every `recall()` call. The semantics are identical — only the parameter name changed.

---

### 2. CostStats.avg_cost removed — use .mean

`CostStats.avg_cost` has been removed. The correct attribute has been `.mean` since v0.11, but `avg_cost` was kept as an alias. It is now gone entirely.

**Before (v0.11):**

```python
stats = agent.cost_stats
print(f"Average cost: ${stats.avg_cost:.6f}")
```

**After (v0.12):**

```python
stats = agent.cost_stats
print(f"Average cost: ${stats.mean:.6f}")
```

`.mean` has been available since v0.11.0. If you were using `.mean` already, no change is needed.

---

### 3. SandboxBackend alias removed — use SandboxBackendType

The `SandboxBackend` name was a deprecated alias for `SandboxBackendType`. It is now removed.

**Before (v0.11):**

```python
from syrin.sandbox import SandboxBackend

sandbox = Sandbox(backend=SandboxBackend.PROCESS)
```

**After (v0.12):**

```python
from syrin.sandbox import SandboxBackendType

sandbox = Sandbox(backend=SandboxBackendType.PROCESS)
```

This affects both the import and any type annotations you may have written. Search for `SandboxBackend` (without the `Type` suffix) and update each occurrence.

---

### 4. @tool(depends_on=) requires ToolSpec objects — string names removed

`@tool(depends_on=["other_tool"])` used to accept plain strings naming other tools. String names are now rejected. You must pass `ToolSpec` objects.

**Before (v0.11):**

```python
from syrin import tool

@tool
def fetch_data(url: str) -> str:
    """Fetch data from a URL."""
    ...

@tool(depends_on=["fetch_data"])   # string name — no longer valid
def analyze_data(url: str) -> str:
    """Analyze data from a URL."""
    ...
```

**After (v0.12):**

```python
from syrin import tool
from syrin.tool import ToolSpec

@tool
def fetch_data(url: str) -> str:
    """Fetch data from a URL."""
    ...

@tool(depends_on=[fetch_data])   # pass the ToolSpec object directly
def analyze_data(url: str) -> str:
    """Analyze data from a URL."""
    ...
```

The decorated function (`fetch_data`) is itself a `ToolSpec` after the `@tool` decorator is applied. Pass it directly. This change removes a whole class of runtime errors where a misspelled string name would silently produce wrong dependency graphs.

---

### 5. DegradePolicy fields removed

Three fields that were deprecated in v0.11 are now fully gone from `DegradePolicy`:

| Removed field | What to do |
|---------------|------------|
| `fallback_model` | Remove — model fallback is handled by `Budget(exceed_policy=ExceedPolicy.SWITCH_MODEL)` |
| `compaction_method` | Remove — context compaction is now always automatic |
| `tool_fallback` | Not removed — see below for current valid fields |

The valid fields in v0.12.0 are: `tool_to_disable`, `restore_when`, and `include_resource_status`.

**Before (v0.11):**

```python
from syrin.guardrails import DegradePolicy

policy = DegradePolicy(
    tool_to_disable="search",
    restore_when="budget_recovered",
    fallback_model="gpt-4o-mini",     # removed
    compaction_method="summarize",    # removed
    include_resource_status=True,
)
```

**After (v0.12):**

```python
from syrin.guardrails import DegradePolicy

policy = DegradePolicy(
    tool_to_disable="search",
    restore_when="budget_recovered",
    include_resource_status=True,
    # fallback_model and compaction_method are gone — just remove them
)
```

If you were relying on `fallback_model` to switch to a cheaper model under budget pressure, use `Budget(exceed_policy=ExceedPolicy.SWITCH_MODEL, fallback_model=Model.OpenAI("gpt-4o-mini"))` instead — this is now the canonical way to configure model fallback.

---

### 6. AgentTeam fully removed

`AgentTeam` was deprecated in v0.11 with a `DeprecationWarning` and the promise of removal in v0.12. That removal has happened. Importing `AgentTeam` now raises `ImportError`.

**Before (v0.11 — with warnings):**

```python
from syrin import AgentTeam

team = AgentTeam(agents=[Researcher(), Writer()], goal="...")
result = await team.run()
```

**After (v0.12):**

```python
from syrin.swarm import Swarm
from syrin.enums import SwarmTopology

swarm = Swarm(
    agents=[Researcher(), Writer()],
    goal="...",
    topology=SwarmTopology.PARALLEL,
)
result = await swarm.run()
```

`Swarm` is a strict superset of `AgentTeam`. Every capability that `AgentTeam` had is available in `Swarm`, plus four additional topologies (CONSENSUS, REFLECTION, ORCHESTRATOR, WORKFLOW), shared budget pools, `AgentRegistry`, A2A messaging, `MonitorLoop`, and `pry=True` support. There is no functionality you lose by migrating.

If you have a large codebase and couldn't migrate before the deadline, check out the [v0.10-to-v0.11 migration guide](/migration/v0.10-to-v0.11) for the full `AgentTeam` → `Swarm` mapping.

---

## New Features in v0.12.0

These are additions — no code changes required to use them.

### exec_bash — run shell scripts safely

`Sandbox` now supports shell script execution. Enable it with `bash=True`:

```python
from syrin.sandbox import Sandbox

sandbox = Sandbox(bash=True, timeout=30.0)

result = await sandbox.exec_bash("""
#!/usr/bin/env bash
set -euo pipefail
echo "workspace: $SANDBOX_WORKSPACE"
seq 1 10 | paste -s -d+ | bc
""")
print(result.stdout)    # "workspace: /tmp/syrin-sandbox-abc\n55\n"
print(result.exit_code) # 0
```

The script runs via `bash` (falling back to `sh`), with the workspace directory set as `$SANDBOX_WORKSPACE`. Temp files are created with `chmod 0o700` and cleaned up after each call.

### packages= — auto-install before first exec

List Python packages at construction time and they are installed automatically before the first `exec_python` call. Installation is thread-safe (double-checked `asyncio.Lock`) and happens only once per instance:

```python
sandbox = Sandbox(packages=["requests", "numpy", "pandas"])

# No manual install() call — packages are ready on first exec
result = await sandbox.exec_python("import numpy; print(numpy.__version__)")
```

### Sandbox async context manager

`Sandbox` is now an async context manager. Use it with `async with` to guarantee workspace cleanup even if an exception occurs:

```python
async with Sandbox(bash=True, packages=["httpx"]) as sb:
    result = await sb.exec_bash("echo hello")
    print(result.stdout)
# Workspace deleted here automatically
```

The manual `cleanup()` method is still available for long-lived sandboxes. It is safe to call multiple times and only removes workspaces that were auto-created (i.e., `workspace=None` was passed to the constructor).

### RLM_BUDGET_SPLIT hook fields corrected

The `RLM_BUDGET_SPLIT` hook now carries accurate fields. If you subscribed to this hook in v0.11, update your handler:

```python
# v0.12 fields: split, parent_budget, child_budget, agent
agent.events.on(
    Hook.RLM_BUDGET_SPLIT,
    lambda ctx: print(
        f"Split ({ctx.split}): parent=${ctx.parent_budget:.4f}, "
        f"{ctx.agent} gets ${ctx.child_budget:.4f}"
    ),
)
```

The old `split_policy` and `children` fields are gone. The `agent` field is the string name of the agent class being spawned.

### Sandbox auto-propagation through RLMLoop

Setting `sandbox=` on an `RLMLoop` now automatically wires the sandbox into every spawned child agent. Children get their own `copy.copy()` of the loop with `sandbox` set — the orchestrator's loop class attribute is never mutated:

```python
from syrin import Agent, Model, RLMLoop
from syrin.sandbox import Sandbox

class Worker(Agent):
    model = Model.OpenAI("gpt-4o-mini")
    # No sandbox= here — wired in from orchestrator

class Orchestrator(Agent):
    model = Model.OpenAI("gpt-4o")
    loop = RLMLoop(
        allowed_agents=[Worker],
        sandbox=Sandbox(bash=True, python=True),
    )
```

---

## Summary: What You Need to Do

| Change | Action |
|--------|--------|
| `recall(count=N)` or `recall(top_k=N)` | Replace with `recall(limit=N)` |
| `stats.avg_cost` | Replace with `stats.mean` |
| `from syrin.sandbox import SandboxBackend` | Replace with `SandboxBackendType` |
| `@tool(depends_on=["name"])` | Replace with `@tool(depends_on=[tool_spec_object])` |
| `DegradePolicy(fallback_model=..., compaction_method=...)` | Remove those fields |
| `from syrin import AgentTeam` | Replace with `Swarm` |

Everything else in v0.11 continues to work unchanged.

## What's Next?

- [Sandbox](/production/sandbox) — full exec_bash reference and end-to-end examples
- [Sub-agent Spawning](/advanced/rlm-loop) — sandbox wiring and updated RLM_BUDGET_SPLIT hook
- [Swarm](/multi-agent/swarm) — full topology reference (AgentTeam replacement)
