Skip to content

base

base

Abstract base for Claude execution backends.

Classes

ExecutionResult dataclass

ExecutionResult(success, stdout, stderr, duration_seconds, exit_code=None, exit_signal=None, exit_reason='completed', started_at=utc_now(), rate_limited=False, rate_limit_wait_seconds=None, error_type=None, error_message=None, model=None, tokens_used=None, input_tokens=None, output_tokens=None)

Result of executing a prompt through a backend.

Captures all relevant output and metadata for validation and debugging.

Note: Fields are ordered with required fields first, then optional fields with defaults, as required by Python dataclasses.

Attributes
success instance-attribute
success

Whether the execution completed without error (exit code 0).

stdout instance-attribute
stdout

Standard output from the command.

stderr instance-attribute
stderr

Standard error from the command.

duration_seconds instance-attribute
duration_seconds

Execution duration in seconds.

exit_code class-attribute instance-attribute
exit_code = None

Process exit code or HTTP status code. None if killed by signal.

exit_signal class-attribute instance-attribute
exit_signal = None

Signal number if process was killed by a signal (e.g., 9=SIGKILL, 15=SIGTERM).

On Unix, when a process is killed by a signal, returncode = -signal_number. This field extracts that signal for clearer diagnostics.

exit_reason class-attribute instance-attribute
exit_reason = 'completed'

Why the execution ended: - completed: Normal exit (exit_code set) - timeout: Process was killed due to timeout - killed: Process was killed by external signal - error: Internal error prevented execution

started_at class-attribute instance-attribute
started_at = field(default_factory=utc_now)

When execution started.

rate_limited class-attribute instance-attribute
rate_limited = False

Whether rate limiting was detected.

rate_limit_wait_seconds class-attribute instance-attribute
rate_limit_wait_seconds = None

Parsed wait duration from rate limit error, in seconds.

When set, this is the actual duration extracted from the API's error message (e.g. 'retry after 300 seconds'). When None, callers should use their default wait time.

error_type class-attribute instance-attribute
error_type = None

Classified error type if failed.

error_message class-attribute instance-attribute
error_message = None

Human-readable error message.

model class-attribute instance-attribute
model = None

Model used for execution.

tokens_used class-attribute instance-attribute
tokens_used = None

Total tokens consumed (API backend only).

.. deprecated:: Use input_tokens + output_tokens instead. This field will be removed in a future version. Equivalent: tokens_used == (input_tokens or 0) + (output_tokens or 0).

input_tokens class-attribute instance-attribute
input_tokens = None

Input tokens consumed (prompt tokens). None if not available from backend.

output_tokens class-attribute instance-attribute
output_tokens = None

Output tokens consumed (completion tokens). None if not available from backend.

output property
output

Combined stdout and stderr.

Functions
__post_init__
__post_init__()

Validate invariant: success=True requires exit_code 0 or None.

Source code in src/marianne/backends/base.py
def __post_init__(self) -> None:
    """Validate invariant: success=True requires exit_code 0 or None."""
    if self.success and self.exit_code is not None and self.exit_code != 0:
        raise ValueError(
            f"Inconsistent ExecutionResult: success=True but exit_code={self.exit_code}. "
            "success=True should only be set when exit_code is 0 or None."
        )

Backend

Bases: ABC

Abstract base class for Claude execution backends.

Backends handle the actual execution of prompts through Claude, whether via CLI subprocess or direct API calls.

Attributes
name abstractmethod property
name

Human-readable backend name.

working_directory property writable
working_directory

Working directory for backend execution.

Subprocess-based backends (e.g. ClaudeCliBackend) use this as the cwd for child processes. API-based backends store it but don't use it directly.

Returns None if no working directory is set, meaning the process CWD is used.

Thread/Concurrency Safety: This property is NOT safe to mutate while executions are in-flight. The worktree isolation layer sets it before any sheet execution starts, and restores it in the finally block after all sheets complete. During parallel execution, all concurrent sheets share the same working directory (the worktree path), so the value is read-only while sheets are running. Never change this property from a concurrent task mid-execution.

override_lock property
override_lock

Lock for serializing apply_overrides → execute → clear_overrides cycles.

Parallel sheet execution must acquire this lock around the entire override lifecycle to prevent concurrent sheets from stomping on each other's saved originals.

Functions
execute abstractmethod async
execute(prompt, *, timeout_seconds=None)

Execute a prompt and return the result.

Parameters:

Name Type Description Default
prompt str

The prompt to send to Claude

required
timeout_seconds float | None

Per-call timeout override. If provided, overrides the backend's default timeout for this single execution.

None

Returns:

Type Description
ExecutionResult

ExecutionResult with output and metadata

Source code in src/marianne/backends/base.py
@abstractmethod
async def execute(
    self, prompt: str, *, timeout_seconds: float | None = None,
) -> ExecutionResult:
    """Execute a prompt and return the result.

    Args:
        prompt: The prompt to send to Claude
        timeout_seconds: Per-call timeout override. If provided, overrides
            the backend's default timeout for this single execution.

    Returns:
        ExecutionResult with output and metadata
    """
    ...
health_check abstractmethod async
health_check()

Check if the backend is available and working.

Used to verify connectivity before starting a job, and to check if rate limits have lifted.

Returns:

Type Description
bool

True if backend is ready, False otherwise

Source code in src/marianne/backends/base.py
@abstractmethod
async def health_check(self) -> bool:
    """Check if the backend is available and working.

    Used to verify connectivity before starting a job,
    and to check if rate limits have lifted.

    Returns:
        True if backend is ready, False otherwise
    """
    ...
availability_check async
availability_check()

Lightweight check: is the backend reachable without consuming API quota?

Unlike health_check(), this must NOT send prompts or consume tokens. Used after quota exhaustion waits where sending a prompt would fail.

Default returns True (assume available). Backends override for real checks.

Source code in src/marianne/backends/base.py
async def availability_check(self) -> bool:
    """Lightweight check: is the backend reachable without consuming API quota?

    Unlike health_check(), this must NOT send prompts or consume tokens.
    Used after quota exhaustion waits where sending a prompt would fail.

    Default returns True (assume available). Backends override for
    real checks.
    """
    return True
close async
close()

Close the backend and release resources.

Override in subclasses that hold persistent connections or resources. Default implementation is a no-op for backends without cleanup needs.

This method should be idempotent - calling it multiple times should be safe.

Source code in src/marianne/backends/base.py
async def close(self) -> None:  # noqa: B027
    """Close the backend and release resources.

    Override in subclasses that hold persistent connections or resources.
    Default implementation is a no-op for backends without cleanup needs.

    This method should be idempotent - calling it multiple times should be safe.
    """
__aenter__ async
__aenter__()

Async context manager entry.

Source code in src/marianne/backends/base.py
async def __aenter__(self) -> Backend:
    """Async context manager entry."""
    return self
__aexit__ async
__aexit__(exc_type, exc_val, exc_tb)

Async context manager exit — ensures close() is called.

Source code in src/marianne/backends/base.py
async def __aexit__(
    self,
    exc_type: type[BaseException] | None,
    exc_val: BaseException | None,
    exc_tb: object,
) -> None:
    """Async context manager exit — ensures close() is called."""
    await self.close()
apply_overrides
apply_overrides(overrides)

Apply per-sheet parameter overrides for the next execution.

Called per-sheet by the runner when sheet_overrides is configured. Subclasses store the overrides and apply them in execute(). clear_overrides() is called after execution to restore defaults.

Callers MUST hold override_lock for the entire apply → execute → clear window when parallel execution is possible.

Default implementation is a no-op for backends without override support.

Parameters:

Name Type Description Default
overrides dict[str, object]

Dict of parameter name → value. Only non-None values from SheetBackendOverride are included.

required
Source code in src/marianne/backends/base.py
def apply_overrides(self, overrides: dict[str, object]) -> None:  # noqa: B027
    """Apply per-sheet parameter overrides for the next execution.

    Called per-sheet by the runner when ``sheet_overrides`` is configured.
    Subclasses store the overrides and apply them in ``execute()``.
    ``clear_overrides()`` is called after execution to restore defaults.

    Callers MUST hold ``override_lock`` for the entire
    apply → execute → clear window when parallel execution is possible.

    Default implementation is a no-op for backends without override support.

    Args:
        overrides: Dict of parameter name → value. Only non-None values
            from ``SheetBackendOverride`` are included.
    """
clear_overrides
clear_overrides()

Clear per-sheet parameter overrides, restoring defaults.

Called after each sheet execution to ensure the next sheet uses global config. Default implementation is a no-op.

Source code in src/marianne/backends/base.py
def clear_overrides(self) -> None:  # noqa: B027
    """Clear per-sheet parameter overrides, restoring defaults.

    Called after each sheet execution to ensure the next sheet uses
    global config. Default implementation is a no-op.
    """
set_preamble
set_preamble(_preamble)

Set the dynamic preamble for the next execution.

Called per-sheet by the runner with a context-aware preamble built by build_preamble(). The preamble includes sheet identity, position, workspace, and retry status.

Override in subclasses that support prompt injection. Default implementation is a no-op for backends without this capability.

Parameters:

Name Type Description Default
_preamble str | None

Preamble text to prepend, or None to clear.

required
Source code in src/marianne/backends/base.py
def set_preamble(self, _preamble: str | None) -> None:  # noqa: B027
    """Set the dynamic preamble for the next execution.

    Called per-sheet by the runner with a context-aware preamble built by
    ``build_preamble()``. The preamble includes sheet identity, position,
    workspace, and retry status.

    Override in subclasses that support prompt injection.
    Default implementation is a no-op for backends without this capability.

    Args:
        _preamble: Preamble text to prepend, or None to clear.
    """
set_prompt_extensions
set_prompt_extensions(_extensions)

Set prompt extensions for the next execution.

Extensions are additional directive blocks injected after the preamble. Called per-sheet by the runner to apply score-level and sheet-level prompt extensions (GH#76).

Override in subclasses that support prompt injection. Default implementation is a no-op for backends without this capability.

Parameters:

Name Type Description Default
_extensions list[str]

List of extension text blocks.

required
Source code in src/marianne/backends/base.py
def set_prompt_extensions(self, _extensions: list[str]) -> None:  # noqa: B027
    """Set prompt extensions for the next execution.

    Extensions are additional directive blocks injected after the
    preamble. Called per-sheet by the runner to apply score-level and
    sheet-level prompt extensions (GH#76).

    Override in subclasses that support prompt injection.
    Default implementation is a no-op for backends without this capability.

    Args:
        _extensions: List of extension text blocks.
    """
set_output_log_path
set_output_log_path(_path)

Set base path for real-time output logging.

Called per-sheet by runner to enable streaming output to log files. This provides visibility into backend output during long executions.

Uses industry-standard separate files for stdout and stderr: - {path}.stdout.log - standard output - {path}.stderr.log - standard error

Override in subclasses that support real-time output streaming. Default implementation is a no-op for backends without this capability.

Parameters:

Name Type Description Default
_path Path | None

Base path for log files (without extension), or None to disable.

required
Source code in src/marianne/backends/base.py
def set_output_log_path(self, _path: Path | None) -> None:  # noqa: B027
    """Set base path for real-time output logging.

    Called per-sheet by runner to enable streaming output to log files.
    This provides visibility into backend output during long executions.

    Uses industry-standard separate files for stdout and stderr:
    - {path}.stdout.log - standard output
    - {path}.stderr.log - standard error

    Override in subclasses that support real-time output streaming.
    Default implementation is a no-op for backends without this capability.

    Args:
        _path: Base path for log files (without extension), or None to disable.
    """

HttpxClientMixin

Shared lazy httpx.AsyncClient lifecycle for HTTP-based backends.

Provides _get_client() for lazy initialization with connection pooling and _close_httpx_client() for cleanup. Both Ollama and RecursiveLight backends use this same pattern.

Functions