Build a private repository cleanup agent
Build a coding agent that improves code in a private GitHub repository and opens a pull request. You define the agent in one project and call it from a separate client project.
The agent works in a fresh managed sandbox. Sandbox Proxy lets the sandbox use your GitHub
connector without copying the GitHub credential into the sandbox. The agent uses the
mistral-large-4 model.
An optional final section shows how to place a custom scheduled workflow on top of the deployed agent. You do not need the scheduler to build, test, or deploy the agent.
Demonstrates: sandboxes and Sandbox Proxy, a timeout on each step, idempotency keys, ending a session, serving and deploying an agent.
Run (after the setup below), from the example's client/ folder:
uv run --frozen --env-file .env python run_agent.py (setup).
How it works
The client creates a session against the agent and sends the cleanup task. The agent clones the repository, creates a branch, makes coherent code cleanups, runs focused checks, and opens a pull request.
client/ → agent/ → MistralSandbox → Sandbox Proxy → private GitHub repository
↓
pull requestThe example keeps each responsibility in a separate folder:
| Folder | Purpose |
|---|---|
agent/ | The Agent() definition, instructions, Dockerfile, and agent dependencies |
client/ | The cleanup job that opens a session, sends the cleanup task, and consumes the result; optionally run on a schedule by Mistral Workflows |
1. Open the example
The example's folder has this structure:
private_repository_cleanup_agent/
├── README.md
├── agent/
│ ├── Dockerfile
│ ├── instructions.md
│ ├── main.py
│ └── pyproject.toml
└── client/
├── cleanup_task.py
├── run_agent.py
├── scheduled_review.py
├── worker.py
├── Dockerfile
└── pyproject.toml2. Prepare your credentials
You need Python 3.12, uv, and a MISTRAL_API_KEY. The key
authenticates the local agent and client, and it also creates the managed sandbox. To serve or
deploy the agent, the key's user needs a workspace role with Workflows access, such as Developer:
see Deploy. For the sandbox, your Mistral identity
also needs the cloud_user workspace role (Mistral Cloud User in the console). See
Requirements to get it.
In Mistral Studio, enable the GitHub connector. The connected GitHub account must be able to clone the repository, push a branch, and open a pull request.
Set up the agent project:
cd agent
cp .env.example .envAdd your API key to agent/.env:
MISTRAL_API_KEY=...3. Configure the repository
Open agent/main.py and replace your-org/your-private-repo with your private repository:
sandbox = agents.MistralSandbox(
network=agents.Network.all(),
header_bindings=(
# Sandbox Proxy gets a token from the GitHub access of the user who starts the session.
agents.GitHubBinding(repos=("your-org/your-private-repo",)),
),
)Sandbox Proxy gets a short-lived token from your GitHub connector and adds it to the sandbox's GitHub
traffic. repos limits Sandbox Proxy to this repository. It does not grant
GitHub permissions. The
GitHub identity connected in Mistral Studio still needs permission to create branches and pull requests.
Make the same repository replacement in client/cleanup_task.py. The client includes the repository
in the task prompt and validates the pull request URL returned by the agent.
4. Define the agent
The agent definition stays in agent/main.py:
agent = agents.Agent(
model="mistral-large-4",
name=AGENT_NAME,
instructions=(PROJECT_DIR / "instructions.md").read_text(encoding="utf-8"),
environment=sandbox,
max_iterations=60,
temperature=0.1,
)The agent/pyproject.toml pins the SDK with the koyeb extra, which the managed sandbox needs.
The instructions tell the agent to read the repository guidance before editing. They also require the agent to make code changes, run focused checks, create a timestamped branch, and open a pull request. They prohibit dependency updates, migrations, generated files, authentication changes, deployment changes, and documentation-only pull requests.
Install the agent project and inspect its configuration:
uv sync
uv run --frozen --env-file .env agents info5. Serve the agent locally
From agent/, start the agent and leave it running:
uv run --frozen --env-file .env agents serveThe agent project contains no client or scheduling code. Its Dockerfile, which agents deploy
builds, also starts only the agent.
6. Start and consume a session
Open another terminal, go to the example's client/ folder, and configure the client project:
cp .env.example .env
uv syncSet MISTRAL_API_KEY and keep AGENT_NAME equal to the name in agent/main.py:
MISTRAL_API_KEY=...
AGENT_NAME=private-repository-cleanup-agentThe client resolves the agent by name and runs the cleanup job in client/cleanup_task.py. The
scheduled workflow below runs the same function:
async def run_cleanup(agent: agents.Agent, job: str) -> str:
"""Run the cleanup in one session until the agent returns a pull request URL.
`job` names this run. The session start and each message carry an idempotency key made from
it, so running the same job again after a crash continues the same session and turns, and
sends nothing twice.
"""
session = await agent.start_session(idempotency_key=job)
try:
prompt = cleanup_prompt()
replies = []
for step in range(MAX_TURNS):
# timeout= stops the wait, not the turn: a turn still running then comes back as Running.
match await session.run(prompt, idempotency_key=f"{job}:{step}", timeout=TURN_TIMEOUT_SECONDS):
case agents.Completed(text=reply):
replies.append(reply or "")
pull_request_url = extract_pull_request_url(reply or "")
if pull_request_url is not None:
return pull_request_url
prompt = CONTINUE_PROMPT
case agents.Running():
raise RuntimeError(f"A turn ran for more than {TURN_TIMEOUT_SECONDS // 60} minutes.")
case agents.Blocked(callbacks=callbacks):
# The agent asks for no approval, so a callback here needs a person.
raise RuntimeError(f"The agent waits for a person: {[callback.kind for callback in callbacks]}")
case agents.Failed(error=error):
raise RuntimeError(f"The agent turn failed: {error.message}")
case state:
raise RuntimeError(f"The agent turn did not complete: {state!r}")
raise RuntimeError(
f"The agent used {MAX_TURNS} turns without returning a pull request URL. "
f"Its replies were:\n{'\n\n'.join(replies)}"
)
finally:
# The job owns its session: end it, which also stops a turn that is still running.
await session.cancel()session.run() waits until the turn ends, or for up to 45 minutes: timeout= stops the wait, not
the turn, so a turn still running comes back as Running. If the agent needs another turn to finish
the branch or pull request, the job continues in the same session and sandbox. The job owns its
session, so it ends it when it is done, which also stops a turn that is still running.
The session start and each message carry an idempotency key made from the job id that
run_agent.py prints. An error or Ctrl-C ends the session. If the process stops before it can, for
example when it is killed or the machine restarts, python run_agent.py --job <id> continues the same
session and sends nothing twice.
Run the client:
uv run --frozen --env-file .env python run_agent.pyThe command finishes by printing a pull request URL:
Each run creates a new agent-cleanup/<UTC timestamp>-<short-name> branch. An earlier cleanup pull
request can remain open without blocking the next run.
7. Deploy the agent
Commit and push agent/ first: the platform builds the pushed commit. Then deploy from the agent/
directory:
cd ../agent
uv run --frozen --env-file .env agents deploy . \
--name private-repository-cleanup-agent \
--hardenedThe deployed agent creates the sandbox with its own service account. The deploy gives that service
account the cloud_user role, so you must have the role yourself.
Keep --hardened. Sandbox Proxy uses the identity of the user who starts the agent session. That
user must have the GitHub connector configured in Mistral Studio.
The client does not change when the agent is deployed. Set AGENT_NAME in client/.env to the
deployed name and run client/run_agent.py again.
Optional: add a scheduled workflow
You can run the same cleanup job on a schedule from a separate Workflows deployment.
Run the cleanup on a schedule
client/scheduled_review.py and client/worker.py are a Workflows worker that runs the same
cleanup job. They are not loaded by the agent and they are not included in the agent deployment.
A scheduled execution runs as its worker's service account, not as the user who created the
schedule. So this optional project stores a user API key in a workspace secret named
SCHEDULED_AGENT_API_KEY. Its activity uses that key to open a separate session against the
hardened agent, and Sandbox Proxy uses that user's GitHub connector. Each attempt runs the same job:
# Up to four turns of up to 45 minutes each. A retry runs the same job: after a worker crash, noticed
# once start_to_close_timeout has passed, it continues the same session and turns instead of
# starting a second cleanup; after an error, which ended the session, it fails at once.
@workflows.activity(
start_to_close_timeout=timedelta(hours=3, minutes=10),
retry_policy_max_attempts=3,
)
async def run_scheduled_cleanup(job: str) -> str:
import os # noqa: PLC0415
from cleanup_task import AGENT_NAME, run_cleanup # noqa: PLC0415
from mistralai import agents # noqa: PLC0415
api_key = os.environ.get(SCHEDULED_AGENT_API_KEY_ENV_VAR, "").strip()
if not api_key:
raise RuntimeError(
f"Bind a user API key to {SCHEDULED_AGENT_API_KEY_ENV_VAR}. "
"The user must have access to the configured GitHub connector."
)
deployed_agent = agents.Agent.load_from(
agent_name=AGENT_NAME,
api_key=api_key,
)
return await run_cleanup(deployed_agent, job)The GitHub credential remains in the connector service. The user API key authenticates the new session as the user who owns that connector.
Each scheduled run is one job. The activity may retry: after a worker crash, which Workflows notices once the activity's time limit has passed, the retry runs the same job, so it continues the same session and turns instead of starting a second cleanup. An error ends the session, so a retry after an error fails at once.
To use the optional scheduler:
- Replace
your-org/your-private-repoinclient/cleanup_task.py, if not done already. - Create a workspace secret containing a user API key whose user has the GitHub connector.
- Create a managed Workflows deployment from the
client/directory. - Bind the workspace secret to
SCHEDULED_AGENT_API_KEY. - In Workflows, schedule
private-repository-cleanup-schedulewith the input{}and a cron expression, such as0 0 * * *(every day at midnight in the time zone selected in Mistral Studio).
The agent and scheduler remain separate managed deployments.
Troubleshooting
MistralSandbox requires koyeb-sdkMistralSandbox requires koyeb-sdk
Run uv sync from agent/, and keep the koyeb extra on mistralai-agents in its
pyproject.toml: the managed sandbox needs it.
Mistral Cloud refused to create the sandbox (HTTP 401) or (HTTP 403)Mistral Cloud refused to create the sandbox (HTTP 401) or (HTTP 403)
Your Mistral identity does not have the cloud_user workspace role (Mistral Cloud User in the
console). Get it as described in
Requirements, then start the session again. For a deployed
agent, deploy it again after you get the role.
The sandbox deployment failed
The sandbox did not start. The session's error says why. If you set a private image, see
Image and setup for the registry secret it needs.
SandboxProxyInitialSetupErrorSandboxProxyInitialSetupError
Check that the GitHub connector is enabled for the Mistral identity associated with the session.
Confirm that the connected GitHub account can access the repository in repos.
The agent can clone but cannot push
The connected GitHub account has read access but not write access, or an organization policy blocks the branch or pull request. Give the account the required repository role or use a repository where it may create branches and pull requests.
The client cannot find the agent
Confirm that AGENT_NAME in client/.env matches the name in agent/main.py when serving locally,
or the name passed to agents deploy when using a managed deployment.
Resources and next steps
- Sandbox explains managed sandboxes and Sandbox Proxy.
- What the agent can use shows how tools and sandboxes are attached to an agent.
- Run modes compares running the agent in your process with deploying it with Studio (self-hosted or Mistral-managed).