Metadata-Version: 2.4
Name: crashoverride
Version: 0.1.0
Summary: A minimal agentic exception handler
Project-URL: Repository, https://bitbucket.org/njatkinson/crashoverride
Author: CrashOverride contributors
License:             DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
                            Version 2, December 2004
        
         Copyright (C) 2026 CrashOverride contributors
        
         Everyone is permitted to copy and distribute verbatim or modified
         copies of this license document, and changing it is allowed as long
         as the name is changed.
        
                    DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
           TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
        
          0. You just DO WHAT THE FUCK YOU WANT TO.
License-File: LICENSE
Classifier: License :: Freely Distributable
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.13
Requires-Dist: openai-agents
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

# CrashOverride

CrashOverride hands a live Python exception to an OpenAI Agents SDK agent and
lets that agent repair the running program.

Create one application-wide `CrashOverride` recipe, then use section-specific
handlers:

```python
from crashoverride import CrashOverride

crash_override = CrashOverride(
    project_prompt="This application calculates and displays financial ratios.",
)

with crash_override.handler(prompt="Set `result` to a usable numeric fallback"):
    result = 10 / 0

print(result)
```

If the block raises a matching exception, CrashOverride creates a fresh agent,
OpenAI client, provider, and set of exception-bound Python tools. A normal agent
return suppresses the exception and execution continues after the entire
protected block. The failed statement is not retried automatically.

Creating `CrashOverride`, creating a handler, entering a handler, and leaving a
clean or nonmatching handler do not create OpenAI resources. Recovery resources
are created only after a matching exception and are discarded afterward. This
makes an application-wide recipe suitable for threaded and pre-fork servers.

The name and the design are literal: this gives model-generated Python control
of the running process. Hack the planet responsibly.

## Install

CrashOverride requires Python 3.13 or newer.

```console
pip install crashoverride
export OPENAI_API_KEY=...
```

When `api_key` is omitted, `OPENAI_API_KEY` is read only if recovery is needed.

## Layered configuration

Application configuration supplies the project context, common tools, default
model, credentials, and default exception types. A handler inherits those
settings and may override scalar values or add tools and MCP servers:

```python
from crashoverride import CrashOverride

crash_override = CrashOverride(
    project_prompt="""
    This is a billing service. Database writes must be idempotent, and a
    recovery must verify whether an operation partially completed.
    """,
    model="fast-recovery-model",
    tools=(read_service_health,),
    mcp_servers=(operations_server,),
)

with crash_override.handler(
    prompt="Restore `invoice` to a persisted Invoice or raise an error.",
    model="strong-recovery-model",
    tools=(read_invoice_audit_log,),
):
    invoice = create_invoice(request)
```

Application and handler prompts remain separate in the agent input as
`Project context` and `Recovery objective`. Tools and MCP servers are additive,
with application-wide entries first. Passing the same object at both levels
includes it only once.

Omitting a scalar handler argument inherits the application value. Passing
`None` explicitly clears an inherited OpenAI setting or model:

```python
with crash_override.handler(
    prompt="Use the SDK default model for this recovery.",
    model=None,
):
    result = fragile_operation()
```

## API

```python
CrashOverride(
    *,
    project_prompt="",
    system_prompt=None,
    model=None,
    api_key=None,
    base_url=None,
    organization=None,
    openai_project=None,
    catch=Exception,
    tools=(),
    mcp_servers=(),
    openai_client_factory=None,
    agent_factory=None,
)
```

- `project_prompt` describes application-wide architecture, invariants, and
  recovery policy.
- `system_prompt` replaces CrashOverride's built-in agent instructions. This is
  an advanced escape hatch and its use is strongly discouraged; prefer
  `project_prompt` and handler `prompt` for normal customization.
- `model` is passed to the Agents SDK `Agent`. `None` lets the SDK resolve its
  default model.
- `api_key`, `base_url`, `organization`, and `openai_project` configure the
  fresh OpenAI client created for a recovery.
- `catch` is an exception type or tuple of exception types.
- `tools` adds common Agents SDK tools after CrashOverride's native Python
  tools.
- `mcp_servers` adds already-connected MCP servers. Their lifecycle and
  process/thread safety remain the caller's responsibility.
- `openai_client_factory` is an advanced dependency seam described below.
- `agent_factory` is an advanced dependency seam described below.

```python
crash_override.handler(
    prompt="",
    *,
    system_prompt=<inherit>,
    model=<inherit>,
    api_key=<inherit>,
    base_url=<inherit>,
    organization=<inherit>,
    openai_project=<inherit>,
    catch=<inherit>,
    tools=(),
    mcp_servers=(),
)
```

Both `with` and `async with` are supported.

### Advanced system-prompt override

CrashOverride includes a system prompt that teaches the agent how recovery
works, how execution resumes, and how to use its Python tools safely enough to
complete or abandon recovery. Replacing it can make recovery ineffective or
cause the original exception to be suppressed without a valid repair.

Use `project_prompt` for application-wide guidance and handler `prompt` for the
local recovery objective. If necessary, `system_prompt` may replace the
built-in prompt globally or for one handler:

```python
crash_override = CrashOverride(system_prompt="A complete custom recovery protocol...")

with crash_override.handler(system_prompt="A handler-specific recovery protocol..."):
    result = fragile_operation()
```

The override is a complete replacement; CrashOverride does not append its
built-in instructions. Passing `system_prompt=None` to a handler restores the
built-in prompt instead of inheriting an application-level override.

## Lazy factories

CrashOverride normally creates a fresh `AsyncOpenAI` client and Agents SDK
`Agent` only when a matching exception fires. Advanced users may replace either
recipe:

```python
from openai import AsyncOpenAI


def make_client(*, api_key, base_url, organization, project):
    return AsyncOpenAI(
        api_key=api_key,
        base_url=base_url,
        organization=organization,
        project=project,
        timeout=20,
    )


crash_override = CrashOverride(openai_client_factory=make_client)
```

The client factory is called once per matching exception and must return a new
`AsyncOpenAI`. CrashOverride owns that client and closes it after recovery,
including when agent construction or execution fails.

The agent factory receives keyword arguments named `name`, `instructions`,
`model`, `tools`, and `mcp_servers` and must return a fresh Agents SDK `Agent`.
It is primarily useful for deterministic testing. Production applications
normally use the default.

## Python tools

Each recovery gets four fresh, exception-bound tools:

| Tool | Result on failure | Intended use |
| --- | --- | --- |
| `eval_python` | Returns a formatted traceback | Inspect and verify |
| `exec_python` | Returns a formatted traceback | Repair and continue working |
| `eval_python_or_raise` | Escapes the agent run | Abandon recovery from an expression |
| `exec_python_or_raise` | Escapes the agent run | Re-raise or replace the exception |

The non-raising tools catch every `BaseException`, allowing the agent to keep
investigating. The `_or_raise` tools let a failure escape through the Agents SDK;
the SDK may wrap it while preserving the exception cause chain.

## Async code

Use `async with` when an event loop is running:

```python
async with crash_override.handler(
    prompt="Set `payload` to an empty mapping",
):
    payload = await fetch_payload()
```

The recovery agent and Python tools run asynchronously. A regular `with`
handler manages its own event loop and therefore cannot be used from an
already-running loop, including Jupyter and async web framework handlers.

## Deliberately unsafe

There is no sandbox, approval gate, rollback, or transaction boundary. The
agent executes arbitrary Python with the process's permissions and can mutate
state, perform I/O, expose data, or corrupt the process. This is the feature.

## Development

The tests are behavioral: they use the public handler API and deterministic
Agents SDK models that issue real tool calls through the real runner. They make
no inference requests.

```console
python3.13 -m venv .venv
.venv/bin/pip install -e '.[dev]'
.venv/bin/pytest
```

Build and validate a release with:

```console
.venv/bin/python -m build
.venv/bin/twine check dist/*
```

### Publishing

Bitbucket Pipelines tests and builds pull requests. After a commit reaches
`main`, it repeats those checks and publishes the wheel and source distribution
to PyPI. Files for a version that is already published are skipped, so increment
the version in `pyproject.toml` when a merge should create a new release.

Publishing requires a secured Bitbucket repository variable named
`PYPI_API_TOKEN`. Its value must be a PyPI API token, including the `pypi-`
prefix. Because a project-scoped token cannot be created before the first
release exists, use an account-scoped token for the initial publication, then
replace it with a token scoped to the `crashoverride` project.

## License

WTFPL. See [`LICENSE`](LICENSE).
