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

# Patterns & Best Practices

> Production patterns for multi-agent Sailbox work and subprocess attach

Two patterns matter most once you move beyond the quickstart: recording
multiple agents in one Voyage and attaching subprocesses to a parent Voyage.
Use decorators for function-shaped work. Use a `with` span only when a
temporary block is clearer than a named function.

## Multi-agent

When one logical task involves multiple cooperating agents (a Reviewer, a
TestRunner, a GitHub-poster), each agent gets its own context, spans, and
attributed work. They all share one Voyage, so the dashboard shows one trace
for the full background agent workload.

```python theme={null}
import sail

sb = sail.Sailbox.create(
    app=sail.App.find(name="review-agent", mint_if_missing=True),
    image=sail.Image.debian_arm64.apt_install("git").build(),
)

voyage = sail.voyage.create(
    name="multi-agent-code-review",
    sailbox_id=sb.sailbox_id,
    metadata={"pr_number": 1234},
)
# create() is used here because the Sailbox is created before the Voyage
# finishes. For simpler scripts, wrap the work in sail.voyage.run(...).

@sail.agent("GitHub", role="source_control")
@sail.span("clone")
def clone_repo():
    sb.exec(
        "git clone --depth 1 https://github.com/public/repo.git /tmp/repo",
        timeout=120,
    ).wait()


@sail.agent("TestRunner", role="executor")
@sail.span("unit-tests")
def run_tests():
    sb.exec("cd /tmp/repo && pytest -q", timeout=600).wait()


@sail.agent("Reviewer", role="reviewer")
@sail.span("draft-review")
def draft_review():
    response = sail.inference.responses.create(
        model="zai-org/GLM-5.2-FP8",
        input="Review the diff in /tmp/repo...",
        background=False,
        timeout=120,
    )
    sail.voyage.event("review.drafted", payload={"response_id": response["id"]})


clone_repo()
run_tests()
draft_review()

voyage.complete(message="review posted")
```

**Naming convention:** the first (and only required) argument is the
display name ("Reviewer"). The stable attribution key is derived from it
automatically. `role=` is the optional cohort taxonomy ("reviewer",
"test\_runner", "source\_control", "executor"). The dashboard groups runs by
name and offers role as a categorical filter. Pass `slug=` (advanced) to pin
the attribution key across display renames.

## Sailbox exec attribution and `.wait()`

Sailbox commands run as first-class Voyage evidence when they happen inside a
Voyage. Keep the agent context active around the call so the dashboard can show
which participant owned the command:

```python theme={null}
@sail.agent("TestRunner", role="executor")
@sail.span("unit-tests")
def run_tests():
    result = sb.exec("pytest -q", timeout=600).wait()
    sail.voyage.event("tests.finished", payload={"exit_code": result.exit_code})
```

`sb.exec(...)` returns immediately with a request handle. For foreground
commands, call `.wait()` to observe completion, return code, and output tails.
If there is no active span, Sail creates an auto-span for the exec. For
foreground commands, that span closes when `.wait()` observes the result.

Use explicit spans for steps you want named in the product. Let auto-spans cover
low-level calls when you are migrating an existing harness and only need
attribution with minimal code changes.

## Child-process attach

When the controller spawns subprocesses (e.g., a parallel test runner),
the subprocess should attach to the parent's Voyage rather than create
its own. The parent exports `SAIL_VOYAGE_ID`. The child calls
`sail.voyage.attach()`, which reads it:

```python theme={null}
# parent.py
import os
import subprocess
import sail

with sail.voyage.run(name="parent-with-children") as voyage:
    @sail.agent("Orchestrator", role="planner")
    @sail.span("spawn-workers")
    def spawn_workers():
        subprocess.run(
            ["python", "worker.py"],
            env={**os.environ, **voyage.child_env()},
            check=True,
        )

    spawn_workers()
```

`child_env()` returns the handoff env (`SAIL_VOYAGE_ID`, plus the active
agent context as the child's `SAIL_AGENT_*` defaults). It returns `{}` when
telemetry is disabled, so the same code runs keyless.

```python theme={null}
# worker.py
import sail

# Reads SAIL_VOYAGE_ID from env and joins the parent's Voyage.
voyage = sail.voyage.attach()

@sail.agent("Worker", role="executor")
@sail.span("do-work")
def do_work():
    sail.voyage.event("worker.tick", payload={"step": 1})


do_work()

# Note: do NOT call voyage.complete() in the child.
# The parent owns terminal status. First-terminal-wins.
```

## Common cross-pattern pitfalls

* **Do not complete the Voyage from inside an agent block.** Call
  `voyage.complete()` at the top level, after all agent contexts have
  exited.
* **Do not reuse a Voyage across logical tasks.** One Voyage per task. If
  the agent does N tasks, record N Voyages.
* **Do not put secrets in event payloads.** Sail applies server-side redaction,
  but the safest pattern is to summarize or hash sensitive values before
  recording them.
* **Do not open more than one Voyage per controller process** unless you
  intentionally have parallel-independent tasks. The SDK has a process-
  global "current Voyage." Multiple Voyages confuse implicit attribution.

## Reference

* [Voyages overview](./voyages)
* [Voyages quickstart](./voyages-quickstart)
* [Sail skills package](https://github.com/sailresearchco/sail-skills)
* [Sailboxes](./sailboxes)
* SDK package: [`sail` on PyPI](https://pypi.org/project/sail/)
