swebot-client
Python client for the swebot autonomous coding agent.
Pure Python — zero dependencies beyond the standard library.
Install
# pip
pip install swebot-client
# uv
uv add swebot-client
Quick start
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:
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:
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:
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:
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.
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
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
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 message in a session of its own → str. A full turn: the agent may act. For a read-only question use session.ask() or start the message with ? |
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.connect("unix:///path/swebot.sock") |
A backend started with --bind unix:///path/swebot.sock, over its Unix socket. SwebotClient("unix:///…") takes the same address; every method works the same. See examples/26_unix_socket.py |
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:
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(here_only=False) |
List sessions → list[dict], each with workdir (where it was created), here (that is the backend's directory) and name (the backend name it last ran under, when it had one); here_only=True keeps just those |
prompt_history(limit=None) |
Prompts a person typed, across all sessions, newest first and each once → list[str] (the TUI's ↑ history; default 500, at most 5000) |
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() |
Re-read config.yaml and reconnect all enabled servers |
Scheduled work
Wake-ups (the agent's schedule tool) and cron jobs run in the backend,
with or without a client connected — nothing here blocks on them. List and
cancel them, and watch them fire with events(). See examples/19_scheduled.py.
Questions
session.ask("what does fitFrame do?") asks a question: it is send("? " + question). The backend answers at once, without the reasoning pre-analysis or the verify pass, and refuses every tool that writes or executes for that turn, so asking never changes files. session.ask(q, keep_mode=True) sends ?? q instead: the same quick answer, but the tools follow the session's mode rather than being blocked, so in autonomous mode the agent may change files if the answer needs it. See examples/20_questions.py.
| Method | Description |
|---|---|
list_wakeups(session_id="") |
Pending wake-ups, soonest first → list[dict] (id, session_id, message, fire_at, repeat_seconds) |
cancel_wakeup(id) |
Cancel a pending wake-up |
list_cron() |
Cron jobs → list[dict] (id, schedule, message, enabled, last_run, next_run) |
add_cron(schedule, message) |
Add a job: "every 5m", "daily at 10:00", "weekdays at 09:00", "once 2026-12-25 10:00" |
enable_cron(id) / disable_cron(id) |
Turn a job on or off, keeping it |
remove_cron(id) |
Delete a job |
events(kinds=None, session_id="", timeout=None) |
Watch the backend's live events → Iterator[Event]; schedule_fired / cron_fired when one starts, message_complete for its reply. Ends at timeout |
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; the mode stays as it is |
backend_inventory() |
Source-owned remote/backend definitions |
backend_action(action, **fields) |
Explicit inventory/lifecycle/connect/uninstall action |
lsp_status() |
Language servers on the backend: running ones, and for each missing one its language name, aliases and exact install commands |
deploy_capabilities() |
Authenticated headless deployment capability check; read-only, no credential transfer |
update_backend() |
Update the backend to the latest release; it restarts in place (refused while turns run) |
shutdown() |
Gracefully stop the backend |
Session
Messaging
| Method | Description |
|---|---|
stream(message) |
Stream tokens → Iterator[str] |
send(message) |
Send and get full reply → str |
send(message, on_question=fn) |
The same, answering the agent's ask_user questions with fn(question, choices) -> answer — also those of the run's continuation steps after send returns, until the run ends (0.14.16) |
wait_until_idle(timeout=None) |
Wait until the session runs nothing; True once idle (0.14.16) |
pending_questions() |
The ask_user questions this session waits on → [{id, question, choices, …}] (0.14.16) |
ask(question) |
Ask a question: read-only, no reasoning passes → str |
ask(question, keep_mode=True) |
Ask a question under the session's current mode (??): no reasoning passes, but in auto the agent may act → 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] |
wakeups() |
This session's pending wake-ups → list[dict] |
restore_checkpoint(id) |
Restore a file to a previous checkpoint |
Context
| Method | Description |
|---|---|
fork_points() |
The prompts the session can be forked in front of → list[dict] (seq, created_at, source, preview, content) |
fork(seq) |
A NEW session with the history before the prompt at seq — take an instruction back; this one is unchanged → Session. See examples/10_sessions.py |
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 |
lsp_diagnostics(server) |
Cached project diagnostic details: file, line, severity and message; no discovery or automatic fixes |
throughput() |
The model's tokens per second in and out: the call running now, the last call, and the last hour over working time only |
message_count |
Number of messages (property) |
Publishing to PyPI
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.
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.
Inspect language-server reports
session.lsp_diagnostics("pyright") reads the backend's cached project
diagnostics without starting a server or asking the agent to repair files.
Each report includes file, line, column, severity and message. Missing imports
can indicate environment setup, not code defects. See examples/22_lsp_diagnostics.py.
Tokens per second
session.throughput() reads GET /api/sessions/{id}/throughput: the model's
speed for this session, measured by the backend per call. Input is input tokens
over the time to the first token (the prompt is read in one go), output is
output tokens over the time after it. now is the call in flight, last the
latest call, and hour every call of the last hour divided by the time the
model was actually working — idle time between turns does not count. A rate
that could not be measured is absent, not zero; {} means no call in the last
hour. See examples/32_throughput.py.
Which language servers are missing, and how to install them
client.lsp_status() reads GET /api/lsp/status. Each availability entry
names the language ("name": "python", "aliases": ["py"]) and, when the
server is missing, install: exact commands, best first, each marked found
when that tool is on the backend's PATH (the backend's machine, which need not
be yours). Nothing is installed. See examples/31_lsp_status.py.
Headless deployment capability
client.deploy_capabilities() reads authenticated GET /api/deploy.
headless_deploy >= 1 supports API-key-only all and independent remote
headless authentication. This check neither deploys nor transfers credentials.
An older backend's HTTP error is preserved: update it rather than falling back
to legacy deployment that might copy OAuth tokens. See
examples/23_deploy_capabilities.py. Use the TUI /backend remote auth REMOTE for
interactive, masked login; no browser is launched automatically.
Interactive deployment HTTP protocol (NDJSON)
See examples/python/deploy_http.py for a
complete standard-library client. Unlike the CLI remote_deploy_pipeline.py, it
keeps the deployment stream open while sending separate answer requests, masks
secret input and cancels on Ctrl+C/EOF. No new public SDK helpers or PyPI release:
0.14.5 already provides deploy_capabilities().
export SWEBOT_ADDR=http://localhost:8080
# Set SWEBOT_TOKEN securely if the source backend requires authentication.
python examples/python/deploy_http.py user@host:2222 --port 8080 --auth anthropic
python examples/python/deploy_http.py user@host --all --auth openai
All requests use the source backend's bearer token. SSH, downloads and saved credential reads run on that backend, not this client. Use trusted HTTPS or a protected local transport for credentials.
- Preflight:
GET /api/deploymust return{"headless_deploy":1}(or a higher supported version). Reject 404/unsupported responses: a legacy backend may ignore auth options and copy OAuth tokens. Capability support does not guarantee deployment availability: POST returns 501 if the backend lacks its deployment binary callback. - Start:
POST /api/deploywith JSON:{"Target":"user@host:2222","Port":8080,"All":false,"Auth":true,"AuthProvider":"anthropic"}
Targetis the SSH destination (2222 is SSH port). RequiredPortis backend port, 1–65535.Allselects saved Anthropic/OpenAI/Google/Alibaba API keys only, never OAuth/login tokens.Authrequests independent remote authentication;AuthProviderselects a provider, or empty string offers a menu. Auth-only (Auth:true, All:false) skips local credential transfer;All:true, Auth:truecombines transfer and login. Without either, offer saved API keys individually. - Read NDJSON (one JSON object per line, not SSE):
{"text":"Installing binary…"} {"text":"Choose provider","question":"opaque-id","input":true,"choices":["anthropic","openai"],"secret":false,"url":"","code":""} {"text":"Enter API key","question":"another-id","input":true,"choices":null,"secret":true,"url":"","code":""} {"text":"Authorize manually","url":"https://provider.example/authorize","code":"DEVICE-CODE"} {"text":"","done":true,"failed":false}
Illustrative events, not a fixed sequence or complete menu. Showtext, URLs and device codes; do not auto-open a browser or use the clipboard. Events withoutquestionrequire no reply. Submit exact displayed choice strings. Masksecret:trueinput; never log answers, bodies or authorization headers. - Answer on a separate connection:
POST /api/deploy/answerwith{"question":"opaque-id","value":"choice or secret"}; response{"accepted":true}. Legacy booleanyesstill supports yes/no prompts; stringvaluetakes precedence. Questions are single-use; expired or consumed IDs return 404. Keep the original streaming request open. - Finish/cancel:
done:true, failed:falsemeans deployment completed, not necessarily backend startup. Installation without usable credentials succeeds unstarted: preserve progress and follow its hints.failed:truecarries sanitized failure text. EOF withoutdoneleaves outcome unknown. Closing the stream requests cancellation; the backend also enforces a 20-minute deadline. Completed installation/settings are not rolled back and existing backends are not stopped. Verify remote state before retrying after cancellation or transport failure.
Remote connection and lifecycle HTTP API
These authenticated routes also execute on the source backend. No public SDK
stream/lifecycle helpers exist; the HTTP example uses urllib. Existing
SwebotClient can use a proxy as its base URL without a new package release.
| Request | Response and semantics |
|---|---|
POST /api/connect with {"Target":"user@host:2222","Port":8080} |
Returns {"id":"…"}; optional Token overrides destination token lookup. Does not start/stop a backend or prove readiness. |
Requests under /api/connect/{id}/… |
Forward through private SSH stdio. Client base URL: source URL + /api/connect/{id}; use source token, proxy supplies destination token. Supports normal API and streaming requests. |
DELETE /api/connect/{id} |
Returns {"closed":true}; removes proxy only, does not stop remote backend. |
POST /api/deploy/lifecycle with {"Target":"user@host","Port":8080,"Operation":"start"} |
Returns {"started":true}. HTTP 502 startup errors can include code: configuration, credentials, port, launch, readiness, protocol. |
Same route with Operation:"stop" |
Token-free version, instance, pid, status, sessions_known and optional latest_session. Status: stopped, shutting_down, not_running, exited. HTTP 200 is not proof shutdown completed; unknown session state is not an empty database. Caller must obtain explicit user consent. |
Connections are process-local, expire after 24 hours and require the source
backend to stay up. Requests forwarded through missing/expired proxies return
404; SSH transport failures return 502. Deletion is idempotent and returns
{"closed":true} even for a missing or expired ID. Reconnect after expiry/source restart. Proxy deletion is not backend
shutdown. The CLI pipeline demonstrates tasks against a running destination;
installation-only success is not proof it is ready for those tasks.
Named backend inventory (0.14.6)
GET /api/backends returns remotes (name -> SSH target) and backends (REMOTE/NAME -> name/remote/workspace/port). POST /api/backends/action takes action and fields: remote-add (name,target), remote-remove/remove (name), save/create-local (backend, optional create_directory; create-local accepts auto_port to skip occupied/saved ports), refresh (optional remote name), prepare/start/stop/logs/ connect (name; create_directory for prepare). Stop is explicit caller authorization; HTTP does not add TUI consent. Refresh returns running/stopped/unreachable per key. Logs returns bounded logs; connect returns proxy id using source bearer token. Existing DELETE /api/connect/{id} closes proxy, not executor. New named databases are isolated; old data untouched. See examples/24_named_backends.py.
Existing HTTP/CLI clients remain compatible despite TUI slash consolidation. Deployment NDJSON adds optional Backend definition and InstallOnly; clients provide workspace and answer directory/credential decisions explicitly.
Authenticated SDK requests and streams reject HTTP redirects rather than forward bearer credentials. Use the backend URL directly, including its proxy base path.
Remote uninstall
Existing backend_action() also supports uninstall on an updated backend/target:
# Destructive operation: call only after obtaining user confirmation.
# Stop target swebot processes first. Normal mode preserves settings/sessions.
client.backend_action("uninstall", name="lab", target="user@host",
full=False, confirmed=True)
# full=True permanently removes standard swebot credentials, settings and databases.
# Workspace files/external databases and source inventory are not deleted.
The target must exactly match backend_inventory()["remotes"]["lab"].
Errors are not converted to success or retried with broader deletion.
No new SDK method or package release is required for this action.
Ask mode (0.14.16)
ask is the backend's fourth mode, next to plan, agent and autonomous. It runs like autonomous — no tool approvals, and the agent keeps going until it is done — but a question the agent asks with its ask_user tool waits for an answer, as long as it takes, instead of being answered "decide yourself". A script that is the user answers them:
client.set_mode("ask")
reply = session.send("Add a config file. Ask me which format.",
on_question=lambda q, choices: choices[0] if choices else "YAML")
In ask mode send returns when the first turn hands off to its continuation steps; on_question keeps answering in the background until the run ends, so call session.wait_until_idle() before switching the mode back or exiting — also when send raised: a stream that dropped after the backend accepted the message leaves its run going. An answer the backend did not accept (a dropped connection) is sent again while the question is open. The responder answers only its own run: once another run claims the session it stops, so a later send is never answered with an earlier callback. on_question(question, choices) returns the answer; a callback that raises still answers ("could not be answered … make your best judgment"), so the turn never waits on a crashed script. Without on_question the question waits for another client — the TUI, an AgentSpeak dashboard — or for client.answer_question(id, answer) with an id from session.pending_questions(). The same goes for plan and agent mode, where ask_user has always waited.
Example: 29_ask_mode.py.
Structured one-shot output (0.14.17)
client.complete() is ONE model request (POST /api/complete): no session, no tools, no retry, no repair, no continuation. Session turns cannot promise that — a turn waits out an overloaded provider and asks again, resends with a lower reasoning effort when one is rejected, and retries an empty answer — so a script that must know what it paid for, or whether the provider enforced its JSON, uses this:
r = client.complete("Label: 'great battery'", json_schema=schema,
schema_name="label", max_output_tokens=2000)
if r.ok:
print(r.parsed, r.attempts, r.usage.output_tokens, r.usage.reasoning_tokens)
else:
print(r.status, r.error or r.validation_errors)
status—ok,invalid_json,schema_mismatch,refusal,truncated,incomplete,errororunsupported. Failures are results, not exceptions; only a malformed request (HTTP 400) raises. A stream that ends before the provider says why it stopped isincomplete, neverok.json_schema— withschema_mode="strict"(default) the provider must enforce it, or the request is refused before generation (status="unsupported",attempts=0).schema_mode="validate"falls back to generating unconstrained and validating locally.schema_enforcementsays which happened (provider_enforced/local_validation_only). The reply is always validated locally too;outputis the raw text, never repaired — a fenced or chatty reply isinvalid_json.max_output_tokens— a provider-side cap; a provider that cannot enforce one refuses the request instead of running uncapped. Reasoning is generated INSIDE the cap on every current provider (output_cap["reasoning_counts"]), so a small cap can be spent entirely on thinking and endtruncatedwith no text.attempts— HTTP requests that reached the provider, counted at the transport: 1, or 0 when refused.request_idsandresponse_idare the provider's ids where it sends them.usage— aUsage:input_tokens,cached_input_tokens(part of input),output_tokens,reasoning_tokens(part of output).Nonemeans not reported — Anthropic does not report reasoning separately, so it isNonethere, not 0; a response without a cache breakdown hascached_input_tokens=None, a stream cut off before its output totaloutput_tokens=None. Figures the provider gave before a failure are kept.partialmarks a failed, cancelled or interrupted request.
client.completion_capabilities() reports what the backend's provider enforces. Today: OpenAI (API key) and Codex (ChatGPT login) enforce both schema and cap through the Responses API; Anthropic (API key and Claude login) enforces the cap through max_tokens on every model and the schema through output_config.format on Claude Haiku 4.5, Sonnet 4.5, Opus 4.1 and later. Copilot, Gemini, Alibaba and local endpoints report unsupported for both until verified — use schema_mode="validate" there. The request uses the backend's current model and reasoning setting.
Session.last_usage is the same Usage for the last send(): every agent run its stream reported (a verify or compaction rerun, a continuation step), each summed over its model calls; reasoning forks and the verify critic are not included. There reported=False means swebot estimated the figures (about 4 characters a token), and reasoning_tokens is None unless every call reported it. The usage stays partial=True until the turn's stream_end arrives, so a turn cut off after a completion (EOF, timeout) never reads as whole.
Example: 30_structured_output.py.
Owner/observer attachments (0.14.8)
Session.attach(client_id=None, observe_only=False) negotiates owner/observer
access and returns a scoped bearer token. It never replaces the shared client's
backend credential. Create a separate SwebotClient with returned token, then a
Session on that client. For SSH proxy URLs the token authenticates both source
proxy and destination session. Never print it or share administrative credentials
as though they were observer credentials.
state() exposes normal runtime snapshot with access_mode. renew_access()
renews scoped access without promotion. release_access() releases it without
cancelling work or deleting history. Retain a unique client_id across reconnects;
do not share one identifier between independent clients. Lease expires after
90 seconds. SDK callers own their renewal loop (every 20 seconds) and release on
exit; no hidden background heartbeat is started.
Example: 25_observe_session.py. This performs no model calls or mutations beyond attachment maintenance.
Files between the backend and this machine (0.14.12)
The TUI's "@" popup can save a backend file here and upload one there; a
script does the same with three methods on SwebotClient. Backend paths are
relative to the backend's working directory.
| Method | What it does |
|---|---|
file_stat(path, include_ignored=False) |
name, dir, size (bytes), files — what a download would copy. A directory counts what the popup shows: no .git, git-ignored files only with include_ignored |
download(path, dest_dir=".", include_ignored=False, timeout=None) |
Copies a file or directory into dest_dir and returns the local path. A directory arrives as one .tar.gz stream and is unpacked as a folder; an entry that would land outside dest_dir is refused |
upload(local_path, dir=".", timeout=None) |
Copies a local file or directory (without .git; hard-linked names each as a file) into dir on the backend; returns path (what an @ mention names), name and size. A read-only attachment gets HTTP 403 |
Nothing is ever overwritten on either side: a second report.pdf becomes
report (2).pdf. Both write under a hidden partial name and rename only when
the copy is complete. timeout defaults to none: it is how long one read
or write on the connection may wait, not a bound on the whole copy, which
takes as long as it takes. The HTTP endpoints are GET /api/files/stat,
GET /api/files/download (a file supports Range, so a client can resume)
and PUT /api/files/upload?dir=&name=[&kind=dir].
Example: 27_files.py.
Images for the model (0.14.15)
| Method | What it does |
|---|---|
upload_image(image, timeout=None) |
Saves an image (a local path, or the bytes) on the backend, as the TUI's Ctrl+V does with a screenshot; returns path, media_type, size. Put the path in a message — the model opens it with read_file, which returns PNG, JPEG, GIF and WebP files as images (scaled to at most 2000 px). Anything the model could not be shown is refused (HTTP 415/422) |
The endpoint is POST /api/images with the image as the body. Saved images
live in ~/.swebot/pastes/images/ on the backend and are deleted after 30
days.
Example: 28_images.py.
Cron responses add RFC3339 next_run_at/last_run_at; convert those instants to
client-local time. Legacy fields remain backend-local, ambiguous strings.
Metadata
Release files for swebot-client 0.14.19
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| swebot_client-0.14.19.tar.gz | 105.3 kB | Without provenance | Details for swebot_client-0.14.19.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| swebot_client-0.14.19-py3-none-any.whl | Python 3 | none | any | Details for swebot_client-0.14.19-py3-none-any.whl |
Total release size: 152.9 kB
Release files / swebot_client-0.14.19.tar.gz
| Download URL | swebot_client-0.14.19.tar.gz |
|---|---|
| Size | 105.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d7a0ac48a45a2ca8e99386942ddfbe786e269573cd65336be817fab5ddc60d3b
|
|
BLAKE2b-256 checksum How to use checksums |
ceb51aca12eddc8ef500ed3ca8eb710f6abc374399bc08fee8bd3ee65542a72d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / swebot_client-0.14.19-py3-none-any.whl
| Download URL | swebot_client-0.14.19-py3-none-any.whl |
|---|---|
| Size | 47.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8a7ff6c55e40892e92c5d7f50e03a79d1a69812c120be5d7fac6155288b23795
|
|
BLAKE2b-256 checksum How to use checksums |
8d9c0e0fa1e9343ec62d5dac75f1320100952d3644163ffffbf1e76b0c7ea1b8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for swebot-client 0.14.19
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.