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,
    base_url: str | None = None,
    aws_profile: str | None = None,
    max_turns: int = 100,
    max_output_tokens: int = 32768,
    max_context_tokens: int | None = None,
    temperature: float | None = None,
    top_p: float = 1.0,
    seed: int | None = None,
    commands: list[str] | str | None = "all",  # "all", "none", "ask", or list (whitelist = run_command only)
    files: str = "some",
    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,
    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,
    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,
)

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", "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.
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.
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.

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) -> 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.

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.