Skip to content

# Python API

Swival exposes a small public API for embedding the agent loop in your own programs. Everything you need is importable from the top-level `swival` package.

    from swival import Session, Result, run
    from swival import AgentError, ConfigError, ContextOverflowError, LifecycleError

## `run(question, *, base_dir=".", **kwargs) -> str`

One-call convenience function. Creates a `Session`, runs the question, and returns the answer string. Raises `AgentError` if the agent exhausts its turns or returns no answer. All keyword arguments are forwarded to `Session`.

    import swival

    answer = swival.run("list the Python files in this directory", provider="lmstudio")
    print(answer)

## `Session`

The main entry point. Stores configuration as plain attributes. Call `.run()` for single-shot questions or `.ask()` for multi-turn conversations.

### Constructor

    Session(
        *,
        base_dir: str = ".",
        provider: str = "lmstudio",
        model: str | None = None,
        api_key: str | None = None,
        user_agent: str | None = None,
        base_url: str | None = None,
        max_turns: int = 100,
        max_output_tokens: int = 32768,
        max_context_tokens: int | None = None,
        max_output_lines: int = 2000,
        max_output_kb: int = 50,
        temperature: float | None = None,
        top_p: float | None = None,
        seed: int | None = None,
        files: str = "some",  # "some", "all", or "none"; yolo=True upgrades an unset value to "all"
        commands: str | list[str] | None = "all",  # "all", "none", "ask", or list (whitelist = run_command only)
        yolo: bool = False,
        verbose: bool = False,
        system_prompt: str | None = None,
        no_system_prompt: bool = False,
        no_instructions: bool = False,
        no_skills: bool = False,
        skills_dir: list[str] | None = None,
        allowed_dirs: list[str] | None = None,
        allowed_dirs_ro: list[str] | None = None,
        sandbox: str = "builtin",
        sandbox_session: str | None = None,
        sandbox_strict_read: bool = False,
        sandbox_auto_session: bool = True,
        nono_profile: str | None = None,
        nono_rollback: bool = False,
        nono_block_net: bool = False,
        nono_allow_domain: list[str] | None = None,
        nono_network_profile: str | None = None,
        nono_credential: list[str] | None = None,
        nono_audit_integrity: bool = False,
        read_guard: bool = True,
        history: bool = True,
        memory: bool = True,
        memory_full: bool = False,
        config_dir: Path | None = None,
        proactive_summaries: bool = False,
        mcp_servers: dict | None = None,
        a2a_servers: dict | None = None,
        extra_body: dict | None = None,
        reasoning_effort: str | None = None,
        continue_here: bool = True,
        sanitize_thinking: bool | None = None,
        show_thinking: bool | None = None,
        prompt_cache: bool = True,
        cache: bool = False,
        cache_dir: str | None = None,
        scratch_dir: str | None = None,
        retries: int = 5,
        encrypt_secrets: bool = False,
        encrypt_secrets_key: str | None = None,
        encrypt_secrets_tweak: str | None = None,
        encrypt_secrets_patterns: list | None = None,
        llm_filter: str | None = None,
        trace_dir: str | None = None,
        subagents: bool = False,
        lifecycle_command: str | None = None,
        lifecycle_timeout: int = 300,
        lifecycle_fail_closed: bool = False,
        lifecycle_enabled: bool = True,
        command_middleware: str | None = None,
        aws_profile: str | None = None,
        project: str | None = None,
        location: str | None = None,
        approved_buckets: set[str] | None = None,
        metaskills: str = "local",  # "local", "all", or "off"
        storm_breaker: bool = True,
        flatten_mcp_schemas: bool = True,
    )

All parameters are keyword-only. The important ones:

Parameter | Description
--- | ---
`base_dir` | Project root. Tools resolve paths relative to this.
`provider` | LLM provider: `"lmstudio"`, `"llamacpp"`, `"huggingface"`, `"openrouter"`, `"chatgpt"`, `"google"`, `"geap"` (`"vertexai"` is an alias), `"bedrock"`, `"generic"`, `"command"`, or a command string.
`model` | Model identifier. Required for most providers; LM Studio and llama.cpp auto-discover.
`api_key` | API key. Can also be set via provider-specific env vars.
`user_agent` | `User-Agent` header for LLM API requests. Defaults to `Swival/<version>`.
`base_url` | Override the provider's default endpoint.
`max_turns` | Maximum agent loop iterations before returning `exhausted=True`.
`max_output_tokens` | Maximum tokens per LLM response.
`max_context_tokens` | Hard cap on context window size. `None` uses the provider's default.
`max_output_lines` | Default number of lines returned by file reads.
`max_output_kb` | Size cap in KB for tool output sent to the model (file reads, grep, listings, outline, fetch_url).
`temperature` | Sampling temperature. `None` uses the provider's default.
`files` | Filesystem access policy: `"some"` (workspace only, the default), `"all"` (unrestricted), or `"none"` (`.swival/` only).
`commands` | Command execution policy: `"all"` (unrestricted, the default), `"none"` (disabled), `"ask"` (interactive approval), or a list of whitelisted command names. With `"all"`, both `run_command` and `run_shell_command` are available. Ask and whitelist modes expose only `run_command`.
`yolo` | Shorthand for `files="all"`. Explicit `files` takes precedence.
`system_prompt` | Override the default system prompt.
`mcp_servers` | MCP server configurations (see MCP).
`a2a_servers` | A2A server configurations (see A2A).
`lifecycle_command` | Shell command to run at startup and exit (see Lifecycle Hooks).
`lifecycle_fail_closed` | If `True`, hook failures raise `LifecycleError` instead of being silently ignored.
`llm_filter` | Path to a filter script that can redact or block outbound LLM requests (see Outbound LLM Filter).
`subagents` | Enable parallel subagent support (`spawn_subagent` / `check_subagents` tools).
`encrypt_secrets` | Enable format-preserving secret encryption (see Secret Encryption).
`retries` | Number of LLM call retries on transient failures. Must be >= 1.
`history` | Write `HISTORY.md` after successful runs.
`memory` | Load memory files (`.swival/memory/`) into the system prompt.
`prompt_cache` | Inject explicit `cache_control` annotations for Anthropic/Gemini/Bedrock. Default `True`. Set `False` to opt out.
`cache` | Cache LLM responses to disk for deterministic replay.
`trace_dir` | Write HuggingFace-compatible JSONL session traces to this directory. Each `run()` call produces a separate file; `ask()` calls accumulate in one file per session.
`verbose` | Print diagnostics to stderr.
`command_middleware` | User-defined script to intercept and rewrite shell commands (see Command Middleware).
`aws_profile` | AWS profile name for the `bedrock` provider.
`approved_buckets` | Pre-approved command buckets for `commands="ask"` mode (e.g. `{"ls", "git status"}`).
`storm_breaker` | Detect repeated identical tool-call failures and inject a corrective hint. Default `True`.
`flatten_mcp_schemas` | Flatten deeply nested MCP tool schemas to formats small models tolerate better. Default `True`.

Parameters not listed here correspond to the same-named CLI flags and config keys. See Customization for the full config reference.

### Streaming and Cancellation Hooks

After constructing a `Session`, you can set two attributes for streaming events and cooperative cancellation. These are used by the A2A server internally, but are available to any library consumer.

    import threading
    from collections.abc import Callable

    session = Session(provider="lmstudio")

    # Stream agent loop events (tool calls, text chunks, status changes).
    def on_event(kind: str, data: dict) -> None:
        print(f"{kind}: {data}")

    session.event_callback = on_event

    # Cancel a running agent loop from another thread.
    stop = threading.Event()
    session.cancel_flag = stop

    # In another thread: stop.set() to request graceful cancellation.

`event_callback` `Callable[[str, dict], None] | None` — called during the agent loop whenever something interesting happens. `kind` is one of:

Kind | Data keys | Description
--- | --- | ---
`"text_chunk"` | `text`, `turn` | Final answer text (emitted when the assistant responds without tool calls).
`"tool_start"` | `name`, `turn` | A tool call is about to execute.
`"tool_finish"` | `name`, `turn`, `elapsed` | A tool call completed. `elapsed` is wall-clock seconds.
`"tool_error"` | `name`, `turn`, `error` | A tool call failed. `error` is the first 500 chars of the error message.
`"status_update"` | `turn`, `max_turns`, `elapsed` | Emitted at the start of each turn with progress info.
`"status_update"` | `turn`, `cancelled` | Emitted when the loop exits due to `cancel_flag`.
`"status_update"` | `turn`, `type` (`"reasoning"`), `text_length` | Emitted when the assistant produces reasoning text alongside tool calls (not a final answer).

Exceptions raised by the callback are silently swallowed — the agent loop never fails because of a callback error.

`cancel_flag` `threading.Event | None` — the agent loop checks this at the start of each turn and between tool calls. When set, the loop exits gracefully at the next check point. The loop does not interrupt a tool call that is already running.

Both default to `None` (no streaming, no external cancellation).

### `Session.run(question, *, report=False) -> Result`

Single-shot execution. Each call is independent — fresh message history, fresh state.

    session = Session(provider="lmstudio")
    result = session.run("refactor the login handler")
    print(result.answer)

If `report=True`, the returned `Result` includes a `report` dict with timing, token usage, and tool stats (see Reports).

The exit lifecycle hook runs automatically after `run()`, even if the agent loop raises.

### `Session.ask(question, *, parse_commands=False) -> Result`

Multi-turn conversation. Shares message history across calls, like the REPL.

    session = Session(provider="lmstudio")
    r1 = session.ask("read src/auth.py and explain the login flow")
    r2 = session.ask("now add rate limiting to that handler")
    session.close(outcome="success", exit_code=0)

On success, the assistant's reply is appended to the shared history so subsequent calls build on prior context.

Set `parse_commands=True` to let an input that begins with a slash command (`/help`), a custom bang command (`!name`), or the quick shell prefix (`!! cmd`) run through the shared command executor instead of being sent to the model verbatim. This is what lets editors speaking ACP reuse the same commands as the REPL. Plain prompts are unaffected.

On failure, the message list is rolled back to its state before the call — including any in-place mutations from compaction — so the session stays usable. State objects (thinking notes, todo items, file tracker) are not rolled back; partial progress from the failed turn is preserved.

Raises `AgentError` (or one of its subclasses) on LLM, tool, or infrastructure failures. In particular, `ContextOverflowError` is raised when the context window is exhausted even after all compaction strategies have been tried. The first `ask()` call triggers setup, so `LifecycleError` can also be raised if a fail-closed startup hook fails.

### `Session.reset()`

Clear the conversation state. The next `ask()` starts with a fresh message history. Setup (provider validation, MCP connections, etc.) is preserved.

### `Session.close(*, outcome=None, exit_code=None)`

Explicitly close the session and run the exit lifecycle hook. Pass `outcome` and `exit_code` to make them available to the hook as `SWIVAL_OUTCOME` and `SWIVAL_EXIT_CODE`.

Idempotent — safe to call after `run()` already ran the exit hook. Resources (MCP servers, cache, secrets) are always cleaned up.

Raises `LifecycleError` if the exit hook fails and `lifecycle_fail_closed` is `True`.

### Context Manager

`Session` supports `with` blocks. The exit hook runs on block exit; resources are cleaned up regardless.

    with Session(lifecycle_command="./sync.sh") as s:
        s.ask("first question")
        s.ask("follow-up")
    # exit hook fires, resources cleaned up

If `lifecycle_fail_closed` is `True` and the exit hook fails, `LifecycleError` propagates from `__exit__` — but only when no other exception is already active.

## `Result`

Returned by `Session.run()` and `Session.ask()`.

    @dataclass
    class Result:
        answer: str | None
        exhausted: bool
        messages: list[dict]
        report: dict | None

Field | Description
--- | ---
`answer` | The agent's final text answer, or `None` if it never produced one.
`exhausted` | `True` if the agent hit `max_turns` without finishing.
`messages` | Deep copy of the full message history (system, user, assistant, tool results).
`report` | Timing and token report dict if `report=True` was passed, otherwise `None`.

## Exceptions

Exceptions from the agent loop and configuration layer are subclasses of `AgentError`. Code that catches `AgentError` handles the common failure modes. Catch a subclass when you need finer control.

The constructor itself may raise standard Python exceptions for argument validation errors (e.g. `ValueError` for `retries < 1`). Unexpected runtime failures from underlying libraries can also escape as their original types — `AgentError` covers swival's own error paths, not every possible exception.

    AgentError
    ├── ConfigError
    ├── ContextOverflowError
    └── LifecycleError

### `AgentError`

Base class for all swival errors that reach the caller. Raised on LLM failures, tool dispatch errors, provider connection problems, and any other runtime failure in the agent loop.

### `ConfigError`

Invalid configuration before the agent loop starts: missing model, bad API key format, malformed MCP server config, non-existent directories in `allowed_dirs`, conflicting options.

`ConfigError` is a subclass of `AgentError`, so `except AgentError` catches it too.

### `ContextOverflowError`

The context window is full and all compaction strategies (message shrinking, turn dropping, tool schema pruning, system prompt truncation) have been exhausted. This means the conversation grew too large for the model's context window and could not be recovered.

Callers that want to handle context exhaustion differently — retry with a larger-context model, split the task, or save partial progress — can catch this specifically:

    from swival import Session, ContextOverflowError

    session = Session(provider="lmstudio")
    try:
        result = session.ask("very large task")
    except ContextOverflowError:
        session.reset()
        result = session.ask("smaller subtask")

### `LifecycleError`

A lifecycle hook (startup or exit) failed while `lifecycle_fail_closed` is `True`. When `lifecycle_fail_closed` is `False` (the default), hook failures are silently ignored and this exception is never raised.

Propagates from `Session.run()`, `Session.ask()` (startup hooks), `Session.close()`, and `Session.__exit__()` (exit hooks). See Lifecycle Hooks for details.

### Internal exceptions

`FilterError`, `McpShutdownError`, and `A2aShutdownError` are caught internally and either wrapped as `AgentError` or handled silently. They are not part of the public API.