Metadata-Version: 2.5
Name: swebot-client
Version: 0.13.0
Summary: Python client for the swebot autonomous coding agent
Project-URL: Homepage, https://github.com/tickup-se/swebot
Project-URL: Repository, https://github.com/tickup-se/swebot
Project-URL: Issues, https://github.com/tickup-se/swebot/issues
Author: Tickup SE
License-Expression: MIT
Keywords: ai,automation,coding-agent,llm,swebot
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# swebot-client

Python client for the [swebot](https://github.com/tickup-se/swebot) autonomous coding agent.

Pure Python — zero dependencies beyond the standard library.

## Install

```bash
# pip
pip install swebot-client

# uv
uv add swebot-client
```

## Quick start

```python
import swebot

print(swebot.ask("explain goroutines"))
```

That is the whole ceremony for a one-shot question against a backend on
localhost. Stream it instead, when you want to watch it arrive:

```python
for token in swebot.stream("explain goroutines"):
    print(token, end="", flush=True)
```

Take a session when the second question depends on the first — a session IS
the context that makes a follow-up a follow-up:

```python
with swebot.session(agent="developer") as s:
    s.send("read parser.go")
    print(s.send("what would you change about it?"))
```

Elsewhere, or behind a token:

```python
swebot.connect("http://10.0.0.5:8080", token="…")   # or set SWEBOT_TOKEN
```

And the full client is one call away — everything below is reachable from it:

```python
client = swebot.Client(timeout=30)
client.set_mode("autonomous")
session = client.create_session(agent="developer")
```

### The same library under two names

`import swebot` is the front door; `swebot_client` is the same objects under
the name existing scripts already import. They can be mixed in one program —
`swebot.TurnFailed` **is** `swebot_client.TurnFailed`, not a copy of it — so
nothing has to be rewritten to use the shorter form, and an `except` clause
keeps catching either way.

```python
from swebot_client import SwebotClient     # unchanged, and staying
```

## Features

- **Streaming** — real-time token-by-token output via SSE
- **Tool callbacks** — observe tool invocations and results
- **Session management** — create, resume, list, delete sessions
- **Mode / model control** — switch modes, models, thinking, reasoning
- **Todos & checkpoints** — read, create, update, delete todos; list/restore checkpoints
- **MCP management** — configure MCP servers programmatically
- **Plugin management** — install, remove, enable, disable plugins
- **AgentSpeak** — multi-agent collaboration (join, leave, send messages)
- **Context control** — compact or clear session context
- **Skills** — list built-in skill agents

## Examples

```python
from swebot_client import SwebotClient, ToolCall

client = SwebotClient("http://localhost:8080")
client.wait_until_ready(timeout=10)

# Set mode to autonomous so the agent executes (not plans)
client.set_mode("autonomous")

# Enable extended thinking
client.set_thinking("high")

# Switch model
client.set_model("claude-opus-4-5")

# Create a session and send a message
session = client.create_session(agent="developer")

def on_tool(tc: ToolCall):
    print(f"  ⚙ {tc.name}")

reply = session.send_with_events(
    "create a hello.py file",
    on_tool_start=on_tool,
)

# Read todos
for todo in session.get_todos():
    print(f"[{todo.status}] {todo.title}")

# Token usage
usage = session.token_usage()
print(f"Tokens: {usage['tokens_in']:,} in / {usage['tokens_out']:,} out ({usage['usage_pct']}%)")
```

### Pipeline example

```python
from swebot_client import SwebotClient

client = SwebotClient()
client.wait_until_ready()
client.set_mode("autonomous")

# Stage 1: generate
s1 = client.create_session()
code = s1.send("Write a Python prime sieve for 0-1000000")

# Stage 2: review
s2 = client.create_session("code_auditor")
review = s2.send(f"Review this code:\n{code}")
print(review)
```

## API reference

### Module-level calls

| Call | Description |
|------|-------------|
| `swebot.ask(message, agent="default", keep=False)` | One question in a session of its own → `str` |
| `swebot.stream(message, agent="default", keep=False)` | The same, token by token → `Iterator[str]` |
| `swebot.session(agent="default", keep=True)` | A session, as a context manager. Kept by default — pass `keep=False` for a scratch one |
| `swebot.connect(addr, token, …)` | Point those three at a different backend → the `Client` |
| `swebot.client()` | The client they are using |

A backend that is not running gives a `ConnectionError` naming the address and
telling you to start one, rather than a bare `URLError` from urllib.

The session `ask` and `stream` create is **scratch**: you never see its id,
nothing can resume it, and a script asking a thousand questions would leave a
thousand sessions behind. It is discarded when the answer is in — unless the
session is still doing something, in which case it is left alone:

- **a wake-up is scheduled** — the turn asked to be resumed, and the session is
  how it comes back. (The same rule swebot's own one-shot CLI applies.)
- **the turn is still running** — an autonomous or goal continuation outlives
  the stream that started it, so a one-shot that set one going leaves it
  working rather than deleting it mid-turn.
- **anything unknown** — an older backend that does not report wake-ups, a
  request that fails. Deleting is the direction that cannot be undone.

The backend checks again as it deletes, so a wake-up scheduled by another
backend between the check and the delete cannot be lost. `keep=True` holds on
to the session regardless; `swebot.session()` is for when the conversation
itself matters.

### `SwebotClient(addr="http://localhost:8080", token="", timeout=60.0, stream_timeout=None)`

No token needed for a backend on your own machine. If it was started with
`--token` (which a backend bound off loopback requires), set `SWEBOT_TOKEN` in
your environment — the SDK reads it automatically.

`timeout` bounds ordinary requests, so a backend that accepts the connection
and then stops answering cannot hang your script; `wait_until_ready(timeout=…)`
passes its remaining time down to each probe. `stream_timeout` is separate and
defaults to *no deadline*, because a turn streams for as long as it runs and a
silent tool call of several minutes is normal.

### Errors

A turn that fails — the provider erroring, a stall, a cancellation — raises
**`TurnFailed`** from `send()`, `stream()` and `send_with_events()`. Whatever
text arrived before the failure is on the exception as `.partial`:

```python
from swebot_client import TurnFailed

try:
    answer = session.send("refactor the parser")
except TurnFailed as e:
    print("the turn failed:", e)
    print("partial output was:", e.partial)
```

These used to return the empty string, or the partial text, with nothing to
distinguish a failed turn from a short answer.

### `ToolCall`

What the agent did, as `on_tool_start` / `on_tool_result` see it: `id`,
`name`, `input`, `output`, and two flags that are NOT the same question.

| Field | Means |
|-------|-------|
| `is_error` | the call did not succeed |
| `not_run` | there was no call — declined, blocked in plan mode, refused by a plugin, cancelled before it started |

A refused tool is not a failed tool, and anything auditing what the agent
actually did needs to tell them apart. `examples/17_failures.py` shows all
three outcomes.

#### Sessions

| Method | Description |
|--------|-------------|
| `create_session(agent)` | Create a new session → `Session` |
| `get_session(id)` | Resume an existing session → `Session` |
| `list_sessions()` | List all sessions |
| `delete_session(id)` | Delete a session |

#### Agents / Providers / Models

| Method | Description |
|--------|-------------|
| `list_agents()` | List available agents |
| `get_agent(name)` | Get agent details (name, system_prompt, tools) |
| `list_providers()` | List LLM providers |
| `list_models(provider)` | List models for a provider → `{provider, active, models}` |
| `set_model(model, provider)` | Switch model (and optionally provider) |

#### Mode / Thinking / Reasoning

| Method | Description |
|--------|-------------|
| `get_mode()` | Current mode (plan, agent, autonomous) |
| `set_mode(mode)` | Set mode: `"plan"`, `"agent"`, `"autonomous"` |
| `get_thinking()` | Current thinking level |
| `set_thinking(level)` | Set thinking: `""`, `"low"`, `"medium"`, `"high"` |
| `get_reasoning()` | Current reasoning depth |
| `set_reasoning(level)` | Set reasoning: `"low"`, `"medium"`, `"high"` |

#### MCP Servers

| Method | Description |
|--------|-------------|
| `list_mcp()` | List MCP servers with connection state |
| `add_mcp(name, ...)` | Add a new MCP server |
| `update_mcp(name, ...)` | Update an existing server |
| `delete_mcp(name)` | Remove a server |
| `enable_mcp(name)` | Enable a disabled server |
| `disable_mcp(name)` | Disable without removing |
| `reload_mcp()` | Reconnect all enabled servers |

#### Plugins

| Method | Description |
|--------|-------------|
| `list_plugins()` | List installed plugins |
| `install_plugin(url)` | Install from GitHub (owner/repo) |
| `remove_plugin(name)` | Uninstall a plugin |
| `enable_plugin(name)` | Re-enable a disabled plugin |
| `disable_plugin(name)` | Disable without removing |

#### AgentSpeak (Multi-Agent)

| Method | Description |
|--------|-------------|
| `agentspeak_status()` | Connection status → `{connected, project, role, url}` |
| `agentspeak_projects()` | List visible projects |
| `agentspeak_who()` | List agents in current project |
| `agentspeak_messages(since)` | Fetch recent messages |
| `agentspeak_join(url, project, role)` | Connect and join a project |
| `agentspeak_leave()` | Disconnect from project |
| `agentspeak_send(message, to)` | Send message (broadcast or targeted) |

#### Backend Management

| Method | Description |
|--------|-------------|
| `health()` | Check if backend is reachable |
| `wait_until_ready(timeout)` | Block until backend responds |
| `stats()` | Backend stats (model, tokens, uptime) |
| `version()` | Version info (version, commit, latest) |
| `skills()` | List built-in skill agents |
| `panic()` | Emergency stop — cancel all, reset to plan mode |
| `shutdown()` | Gracefully stop the backend |

### `Session`

#### Messaging

| Method | Description |
|--------|-------------|
| `stream(message)` | Stream tokens → `Iterator[str]` |
| `send(message)` | Send and get full reply → `str` |
| `send_with_events(msg, on_token, on_tool_start, on_tool_result)` | Send with callbacks → `str` |

#### Todos

| Method | Description |
|--------|-------------|
| `get_todos()` | List todo items → `list[TodoItem]` |
| `create_todo(title)` | Create a new todo |
| `update_todo(id, status)` | Update status: pending, in_progress, done |
| `delete_todo(id)` | Delete a todo |

#### Checkpoints

| Method | Description |
|--------|-------------|
| `list_checkpoints()` | List file checkpoints → `list[dict]` |
| `restore_checkpoint(id)` | Restore a file to a previous checkpoint |

#### Context

| Method | Description |
|--------|-------------|
| `compact()` | Trigger context compaction |
| `clear()` | Clear all session context |
| `token_usage()` | THIS session's usage → `{tokens_in, tokens_out, context_window, usage_pct}` — falls when the session is compacted |
| `backend_token_totals()` | The backend's cumulative counters across every session |

#### Info

| Method | Description |
|--------|-------------|
| `refresh()` | Reload session data from backend |
| `message_count` | Number of messages (property) |

## Publishing to PyPI

```bash
cd sdk/python
python3 -m build
UV_PUBLISH_TOKEN=pypi-YOUR-TOKEN uv publish dist/*
```

## Requirements

- Python 3.9+
- A running swebot backend (`swebot --serve`)

## License

MIT

## Code review

Review what a session changed, then choose what to fix. Findings are verified
by an adversarial second pass — only `confirmed` ones can be applied.

```python
from swebot_client import SwebotClient

session = SwebotClient().create_session()
session.send("Add a ParseInt helper to util/parse.go")

rev = session.review(mode="informative")   # or "autonomous"
if rev.running:
    rev = session.wait_for_review(rev.id)

for f in rev.findings:
    print(f.id, f.severity, f.file, f.claim, "confirmed" if f.confirmed else "refuted")

keep = [f.id for f in rev.confirmed if f.blocking]
drop = [f.id for f in rev.confirmed if not f.blocking]
if keep:
    session.apply_review(rev.id, keep)
if drop:
    session.dismiss_findings(rev.id, drop, reason="not now")
```

A session that changed no files returns `scope="out_of_scope"` (or
`"nothing_to_review"`) without calling a model at all.
