swebot-client 0.9.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Binary
|
|
2
|
+
/sweet
|
|
3
|
+
/swebot
|
|
4
|
+
|
|
5
|
+
# Go build artifacts
|
|
6
|
+
*.exe
|
|
7
|
+
*.exe~
|
|
8
|
+
*.dll
|
|
9
|
+
*.so
|
|
10
|
+
*.dylib
|
|
11
|
+
*.test
|
|
12
|
+
*.out
|
|
13
|
+
|
|
14
|
+
# Go workspace
|
|
15
|
+
/vendor/
|
|
16
|
+
go.work
|
|
17
|
+
go.work.sum
|
|
18
|
+
|
|
19
|
+
# Database
|
|
20
|
+
*.db
|
|
21
|
+
*.db-shm
|
|
22
|
+
*.db-wal
|
|
23
|
+
|
|
24
|
+
# Config with secrets (keep config.yaml.example, ignore real one)
|
|
25
|
+
~/.config/swebot/config.yaml
|
|
26
|
+
|
|
27
|
+
# OS / editor
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
.idea/
|
|
31
|
+
.vscode/
|
|
32
|
+
*.swp
|
|
33
|
+
*.swo
|
|
34
|
+
*~
|
|
35
|
+
|
|
36
|
+
# Session state (Copilot CLI)
|
|
37
|
+
.copilot/
|
|
38
|
+
startme.txt
|
|
39
|
+
|
|
40
|
+
# Python virtualenvs
|
|
41
|
+
.venv/
|
|
42
|
+
__pycache__/
|
|
43
|
+
|
|
44
|
+
# Review notes (not for repo)
|
|
45
|
+
FINDINGS.md
|
|
46
|
+
copilot-session-*.md
|
|
47
|
+
|
|
48
|
+
# SWE-bench evaluation artifacts
|
|
49
|
+
examples/swe-bench/results/
|
|
50
|
+
examples/swe-bench/.venv/
|
|
51
|
+
logs/
|
|
52
|
+
swebot.swebot-eval*.json
|
|
53
|
+
claude-code/
|
|
54
|
+
*_verbose_debug.log
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: swebot-client
|
|
3
|
+
Version: 0.9.7
|
|
4
|
+
Summary: Python client for the swebot autonomous coding agent
|
|
5
|
+
Project-URL: Homepage, https://github.com/tickup-se/swebot
|
|
6
|
+
Project-URL: Repository, https://github.com/tickup-se/swebot
|
|
7
|
+
Project-URL: Issues, https://github.com/tickup-se/swebot/issues
|
|
8
|
+
Author: Tickup SE
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: ai,automation,coding-agent,llm,swebot
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# swebot-client
|
|
25
|
+
|
|
26
|
+
Python client for the [swebot](https://github.com/tickup-se/swebot) autonomous coding agent.
|
|
27
|
+
|
|
28
|
+
Pure Python — zero dependencies beyond the standard library.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# pip
|
|
34
|
+
pip install swebot-client
|
|
35
|
+
|
|
36
|
+
# uv
|
|
37
|
+
uv add swebot-client
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from swebot_client import SwebotClient
|
|
44
|
+
|
|
45
|
+
client = SwebotClient("http://localhost:8080")
|
|
46
|
+
session = client.create_session()
|
|
47
|
+
|
|
48
|
+
# Stream tokens
|
|
49
|
+
for token in session.stream("explain goroutines"):
|
|
50
|
+
print(token, end="", flush=True)
|
|
51
|
+
|
|
52
|
+
# Or get the full reply at once
|
|
53
|
+
reply = session.send("what is 2+2?")
|
|
54
|
+
print(reply)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
- **Streaming** — real-time token-by-token output via SSE
|
|
60
|
+
- **Tool callbacks** — observe tool invocations and results
|
|
61
|
+
- **Session management** — create, resume, list, delete sessions
|
|
62
|
+
- **Todos** — read and update agent todo items
|
|
63
|
+
- **MCP management** — configure MCP servers programmatically
|
|
64
|
+
- **Context control** — compact or clear session context
|
|
65
|
+
- **Skills** — list and use built-in skill agents
|
|
66
|
+
|
|
67
|
+
## Examples
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from swebot_client import SwebotClient, Session, ToolCall, TodoItem
|
|
71
|
+
|
|
72
|
+
client = SwebotClient("http://localhost:8080")
|
|
73
|
+
|
|
74
|
+
# Wait for backend to be ready
|
|
75
|
+
client.wait_until_ready(timeout=10)
|
|
76
|
+
|
|
77
|
+
# Create a session with a specific agent
|
|
78
|
+
session = client.create_session(agent="developer")
|
|
79
|
+
|
|
80
|
+
# Stream with tool call callbacks
|
|
81
|
+
def on_tool(tc: ToolCall):
|
|
82
|
+
print(f" ⚙ {tc.name}")
|
|
83
|
+
|
|
84
|
+
reply = session.send_with_events(
|
|
85
|
+
"create a hello.py file",
|
|
86
|
+
on_tool_start=on_tool,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Read todos
|
|
90
|
+
for todo in session.get_todos():
|
|
91
|
+
print(f"[{todo.status}] {todo.title}")
|
|
92
|
+
|
|
93
|
+
# List MCP servers
|
|
94
|
+
for mcp in client.list_mcp():
|
|
95
|
+
print(f"{mcp['name']}: {'connected' if mcp.get('connected') else 'disconnected'}")
|
|
96
|
+
|
|
97
|
+
# Token usage
|
|
98
|
+
usage = session.token_usage()
|
|
99
|
+
print(f"Tokens: {usage['tokens_in']:,} in / {usage['tokens_out']:,} out ({usage['usage_pct']}%)")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## API reference
|
|
103
|
+
|
|
104
|
+
### `SwebotClient(addr, token="")`
|
|
105
|
+
|
|
106
|
+
| Method | Description |
|
|
107
|
+
|--------|-------------|
|
|
108
|
+
| `health()` | Check if backend is reachable |
|
|
109
|
+
| `wait_until_ready(timeout)` | Block until backend responds |
|
|
110
|
+
| `create_session(agent)` | Create a new session → `Session` |
|
|
111
|
+
| `get_session(id)` | Resume an existing session → `Session` |
|
|
112
|
+
| `list_sessions()` | List all sessions |
|
|
113
|
+
| `delete_session(id)` | Delete a session |
|
|
114
|
+
| `list_agents()` | List available agents |
|
|
115
|
+
| `list_providers()` | List LLM providers |
|
|
116
|
+
| `stats()` | Backend stats (model, tokens, uptime) |
|
|
117
|
+
| `skills()` | List built-in skill agents |
|
|
118
|
+
| `list_mcp()` | List MCP servers |
|
|
119
|
+
| `add_mcp(...)` | Add an MCP server |
|
|
120
|
+
| `delete_mcp(name)` | Remove an MCP server |
|
|
121
|
+
| `shutdown()` | Gracefully stop the backend |
|
|
122
|
+
|
|
123
|
+
### `Session`
|
|
124
|
+
|
|
125
|
+
| Method | Description |
|
|
126
|
+
|--------|-------------|
|
|
127
|
+
| `stream(message)` | Stream tokens → `Iterator[str]` |
|
|
128
|
+
| `send(message)` | Send and get full reply → `str` |
|
|
129
|
+
| `send_with_events(message, ...)` | Send with tool callbacks → `str` |
|
|
130
|
+
| `get_todos()` | List todo items → `list[TodoItem]` |
|
|
131
|
+
| `update_todo(id, status)` | Update a todo's status |
|
|
132
|
+
| `compact()` | Trigger context compaction |
|
|
133
|
+
| `clear()` | Clear all session context |
|
|
134
|
+
| `token_usage()` | Get token usage stats |
|
|
135
|
+
|
|
136
|
+
## Requirements
|
|
137
|
+
|
|
138
|
+
- Python 3.9+
|
|
139
|
+
- A running swebot backend (`swebot --serve`)
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# swebot-client
|
|
2
|
+
|
|
3
|
+
Python client for the [swebot](https://github.com/tickup-se/swebot) autonomous coding agent.
|
|
4
|
+
|
|
5
|
+
Pure Python — zero dependencies beyond the standard library.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# pip
|
|
11
|
+
pip install swebot-client
|
|
12
|
+
|
|
13
|
+
# uv
|
|
14
|
+
uv add swebot-client
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from swebot_client import SwebotClient
|
|
21
|
+
|
|
22
|
+
client = SwebotClient("http://localhost:8080")
|
|
23
|
+
session = client.create_session()
|
|
24
|
+
|
|
25
|
+
# Stream tokens
|
|
26
|
+
for token in session.stream("explain goroutines"):
|
|
27
|
+
print(token, end="", flush=True)
|
|
28
|
+
|
|
29
|
+
# Or get the full reply at once
|
|
30
|
+
reply = session.send("what is 2+2?")
|
|
31
|
+
print(reply)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **Streaming** — real-time token-by-token output via SSE
|
|
37
|
+
- **Tool callbacks** — observe tool invocations and results
|
|
38
|
+
- **Session management** — create, resume, list, delete sessions
|
|
39
|
+
- **Todos** — read and update agent todo items
|
|
40
|
+
- **MCP management** — configure MCP servers programmatically
|
|
41
|
+
- **Context control** — compact or clear session context
|
|
42
|
+
- **Skills** — list and use built-in skill agents
|
|
43
|
+
|
|
44
|
+
## Examples
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from swebot_client import SwebotClient, Session, ToolCall, TodoItem
|
|
48
|
+
|
|
49
|
+
client = SwebotClient("http://localhost:8080")
|
|
50
|
+
|
|
51
|
+
# Wait for backend to be ready
|
|
52
|
+
client.wait_until_ready(timeout=10)
|
|
53
|
+
|
|
54
|
+
# Create a session with a specific agent
|
|
55
|
+
session = client.create_session(agent="developer")
|
|
56
|
+
|
|
57
|
+
# Stream with tool call callbacks
|
|
58
|
+
def on_tool(tc: ToolCall):
|
|
59
|
+
print(f" ⚙ {tc.name}")
|
|
60
|
+
|
|
61
|
+
reply = session.send_with_events(
|
|
62
|
+
"create a hello.py file",
|
|
63
|
+
on_tool_start=on_tool,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Read todos
|
|
67
|
+
for todo in session.get_todos():
|
|
68
|
+
print(f"[{todo.status}] {todo.title}")
|
|
69
|
+
|
|
70
|
+
# List MCP servers
|
|
71
|
+
for mcp in client.list_mcp():
|
|
72
|
+
print(f"{mcp['name']}: {'connected' if mcp.get('connected') else 'disconnected'}")
|
|
73
|
+
|
|
74
|
+
# Token usage
|
|
75
|
+
usage = session.token_usage()
|
|
76
|
+
print(f"Tokens: {usage['tokens_in']:,} in / {usage['tokens_out']:,} out ({usage['usage_pct']}%)")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## API reference
|
|
80
|
+
|
|
81
|
+
### `SwebotClient(addr, token="")`
|
|
82
|
+
|
|
83
|
+
| Method | Description |
|
|
84
|
+
|--------|-------------|
|
|
85
|
+
| `health()` | Check if backend is reachable |
|
|
86
|
+
| `wait_until_ready(timeout)` | Block until backend responds |
|
|
87
|
+
| `create_session(agent)` | Create a new session → `Session` |
|
|
88
|
+
| `get_session(id)` | Resume an existing session → `Session` |
|
|
89
|
+
| `list_sessions()` | List all sessions |
|
|
90
|
+
| `delete_session(id)` | Delete a session |
|
|
91
|
+
| `list_agents()` | List available agents |
|
|
92
|
+
| `list_providers()` | List LLM providers |
|
|
93
|
+
| `stats()` | Backend stats (model, tokens, uptime) |
|
|
94
|
+
| `skills()` | List built-in skill agents |
|
|
95
|
+
| `list_mcp()` | List MCP servers |
|
|
96
|
+
| `add_mcp(...)` | Add an MCP server |
|
|
97
|
+
| `delete_mcp(name)` | Remove an MCP server |
|
|
98
|
+
| `shutdown()` | Gracefully stop the backend |
|
|
99
|
+
|
|
100
|
+
### `Session`
|
|
101
|
+
|
|
102
|
+
| Method | Description |
|
|
103
|
+
|--------|-------------|
|
|
104
|
+
| `stream(message)` | Stream tokens → `Iterator[str]` |
|
|
105
|
+
| `send(message)` | Send and get full reply → `str` |
|
|
106
|
+
| `send_with_events(message, ...)` | Send with tool callbacks → `str` |
|
|
107
|
+
| `get_todos()` | List todo items → `list[TodoItem]` |
|
|
108
|
+
| `update_todo(id, status)` | Update a todo's status |
|
|
109
|
+
| `compact()` | Trigger context compaction |
|
|
110
|
+
| `clear()` | Clear all session context |
|
|
111
|
+
| `token_usage()` | Get token usage stats |
|
|
112
|
+
|
|
113
|
+
## Requirements
|
|
114
|
+
|
|
115
|
+
- Python 3.9+
|
|
116
|
+
- A running swebot backend (`swebot --serve`)
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "swebot-client"
|
|
7
|
+
version = "0.9.7"
|
|
8
|
+
description = "Python client for the swebot autonomous coding agent"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Tickup SE" }]
|
|
13
|
+
keywords = ["swebot", "coding-agent", "llm", "ai", "automation"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/tickup-se/swebot"
|
|
29
|
+
Repository = "https://github.com/tickup-se/swebot"
|
|
30
|
+
Issues = "https://github.com/tickup-se/swebot/issues"
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
swebot-client — Python client for the swebot autonomous coding agent.
|
|
4
|
+
|
|
5
|
+
Pure Python — zero dependencies beyond the standard library.
|
|
6
|
+
|
|
7
|
+
Install:
|
|
8
|
+
pip install swebot-client
|
|
9
|
+
uv add swebot-client
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from swebot_client import SwebotClient
|
|
13
|
+
|
|
14
|
+
client = SwebotClient("http://localhost:8080")
|
|
15
|
+
session = client.create_session()
|
|
16
|
+
for chunk in session.stream("explain goroutines"):
|
|
17
|
+
print(chunk, end="", flush=True)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__version__ = "0.9.7"
|
|
21
|
+
__all__ = ["SwebotClient", "Session", "ToolCall", "TodoItem", "Message", "Event"]
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
import urllib.error
|
|
27
|
+
import urllib.request
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from typing import Callable, Dict, Iterator, List, Optional, Union
|
|
30
|
+
|
|
31
|
+
# ── ANSI colours ──────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
RESET = "\033[0m"
|
|
34
|
+
BOLD = "\033[1m"
|
|
35
|
+
DIM = "\033[2m"
|
|
36
|
+
RED = "\033[91m"
|
|
37
|
+
GREEN = "\033[92m"
|
|
38
|
+
YELLOW = "\033[93m"
|
|
39
|
+
BLUE = "\033[94m"
|
|
40
|
+
MAGENTA = "\033[95m"
|
|
41
|
+
CYAN = "\033[96m"
|
|
42
|
+
WHITE = "\033[97m"
|
|
43
|
+
|
|
44
|
+
def _color(text: str, *codes: str) -> str:
|
|
45
|
+
if not sys.stdout.isatty():
|
|
46
|
+
return text
|
|
47
|
+
return "".join(codes) + text + RESET
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── Data types ─────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ToolCall:
|
|
54
|
+
id: str
|
|
55
|
+
name: str
|
|
56
|
+
input: str = ""
|
|
57
|
+
output: str = ""
|
|
58
|
+
is_error: bool = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class TodoItem:
|
|
63
|
+
id: str
|
|
64
|
+
title: str
|
|
65
|
+
description: str
|
|
66
|
+
status: str # pending | in_progress | done
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class Message:
|
|
71
|
+
role: str
|
|
72
|
+
content: str
|
|
73
|
+
tool_calls: list = field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class Event:
|
|
78
|
+
kind: str
|
|
79
|
+
data: dict
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── Client ────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
class SwebotClient:
|
|
85
|
+
"""HTTP client for the swebot backend API."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, addr: str = "http://localhost:8080", token: str = ""):
|
|
88
|
+
self.addr = addr.rstrip("/")
|
|
89
|
+
self.token = token
|
|
90
|
+
|
|
91
|
+
# ── Helpers ───────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
def _get(self, path: str) -> Dict:
|
|
94
|
+
return self._request("GET", path)
|
|
95
|
+
|
|
96
|
+
def _delete(self, path: str) -> None:
|
|
97
|
+
self._request("DELETE", path)
|
|
98
|
+
|
|
99
|
+
def _request(self, method: str, path: str, body=None) -> Dict:
|
|
100
|
+
url = f"{self.addr}{path}"
|
|
101
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
102
|
+
req = urllib.request.Request(url, method=method, data=data)
|
|
103
|
+
req.add_header("Content-Type", "application/json")
|
|
104
|
+
if self.token:
|
|
105
|
+
req.add_header("Authorization", f"Bearer {self.token}")
|
|
106
|
+
try:
|
|
107
|
+
with urllib.request.urlopen(req) as resp:
|
|
108
|
+
body = resp.read()
|
|
109
|
+
return json.loads(body) if body else {}
|
|
110
|
+
except urllib.error.HTTPError as e:
|
|
111
|
+
raise RuntimeError(f"HTTP {e.code} {method} {path}: {e.read().decode()}") from e
|
|
112
|
+
|
|
113
|
+
def _stream(self, path: str, body: dict) -> Iterator[Event]:
|
|
114
|
+
url = f"{self.addr}{path}"
|
|
115
|
+
data = json.dumps(body).encode()
|
|
116
|
+
req = urllib.request.Request(url, method="POST", data=data)
|
|
117
|
+
req.add_header("Content-Type", "application/json")
|
|
118
|
+
if self.token:
|
|
119
|
+
req.add_header("Authorization", f"Bearer {self.token}")
|
|
120
|
+
event_type = ""
|
|
121
|
+
with urllib.request.urlopen(req) as resp:
|
|
122
|
+
for raw in resp:
|
|
123
|
+
line = raw.decode().rstrip("\n")
|
|
124
|
+
if line.startswith("event: "):
|
|
125
|
+
event_type = line[7:]
|
|
126
|
+
elif line.startswith("data: ") and event_type:
|
|
127
|
+
yield Event(kind=event_type, data=json.loads(line[6:]))
|
|
128
|
+
event_type = ""
|
|
129
|
+
|
|
130
|
+
# ── Health ────────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
def health(self) -> bool:
|
|
133
|
+
try:
|
|
134
|
+
self._request("GET", "/health")
|
|
135
|
+
return True
|
|
136
|
+
except Exception:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def wait_until_ready(self, timeout: float = 10.0) -> bool:
|
|
140
|
+
deadline = time.time() + timeout
|
|
141
|
+
while time.time() < deadline:
|
|
142
|
+
if self.health():
|
|
143
|
+
return True
|
|
144
|
+
time.sleep(0.2)
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
# ── Sessions ──────────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
def create_session(self, agent: str = "default") -> "Session":
|
|
150
|
+
resp = self._request("POST", "/api/sessions", {"agent": agent})
|
|
151
|
+
return Session(self, resp["id"], agent)
|
|
152
|
+
|
|
153
|
+
def get_session(self, session_id: str) -> "Session":
|
|
154
|
+
resp = self._request("GET", f"/api/sessions/{session_id}")
|
|
155
|
+
return Session(self, resp["id"], resp.get("agent", "default"), resp)
|
|
156
|
+
|
|
157
|
+
def list_sessions(self) -> List[dict]:
|
|
158
|
+
resp = self._request("GET", "/api/sessions")
|
|
159
|
+
return resp if isinstance(resp, list) else resp.get("sessions", [])
|
|
160
|
+
|
|
161
|
+
def delete_session(self, session_id: str) -> None:
|
|
162
|
+
self._request("DELETE", f"/api/sessions/{session_id}")
|
|
163
|
+
|
|
164
|
+
# ── Agents / providers ────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
def list_agents(self) -> List[dict]:
|
|
167
|
+
resp = self._request("GET", "/api/agents")
|
|
168
|
+
return resp if isinstance(resp, list) else resp.get("agents", [])
|
|
169
|
+
|
|
170
|
+
def list_providers(self) -> List[dict]:
|
|
171
|
+
resp = self._request("GET", "/api/providers")
|
|
172
|
+
return resp if isinstance(resp, list) else resp.get("providers", [])
|
|
173
|
+
|
|
174
|
+
# ── MCP management ────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
def list_mcp(self) -> List[dict]:
|
|
177
|
+
"""Return all configured MCP servers with live connection state."""
|
|
178
|
+
resp = self._request("GET", "/api/mcp")
|
|
179
|
+
return resp if isinstance(resp, list) else resp.get("servers", [])
|
|
180
|
+
|
|
181
|
+
def add_mcp(
|
|
182
|
+
self,
|
|
183
|
+
name: str,
|
|
184
|
+
*,
|
|
185
|
+
type: str = "stdio",
|
|
186
|
+
command: str = "",
|
|
187
|
+
args: Optional[List[str]] = None,
|
|
188
|
+
url: str = "",
|
|
189
|
+
env: Optional[List[str]] = None,
|
|
190
|
+
headers: Optional[Dict[str, str]] = None,
|
|
191
|
+
tools: Optional[List[str]] = None,
|
|
192
|
+
disabled: bool = False,
|
|
193
|
+
) -> Dict:
|
|
194
|
+
"""Add a new MCP server and connect it immediately."""
|
|
195
|
+
payload: dict = {"name": name, "type": type}
|
|
196
|
+
if command:
|
|
197
|
+
payload["command"] = command
|
|
198
|
+
if args:
|
|
199
|
+
payload["args"] = args
|
|
200
|
+
if url:
|
|
201
|
+
payload["url"] = url
|
|
202
|
+
if env:
|
|
203
|
+
payload["env"] = env
|
|
204
|
+
if headers:
|
|
205
|
+
payload["headers"] = headers
|
|
206
|
+
if tools:
|
|
207
|
+
payload["tools"] = tools
|
|
208
|
+
if disabled:
|
|
209
|
+
payload["disabled"] = True
|
|
210
|
+
return self._request("POST", "/api/mcp", payload)
|
|
211
|
+
|
|
212
|
+
def update_mcp(self, name: str, **fields) -> Dict:
|
|
213
|
+
"""Update an existing MCP server's config (reconnects automatically)."""
|
|
214
|
+
fields["name"] = name
|
|
215
|
+
return self._request("PUT", f"/api/mcp/{name}", fields)
|
|
216
|
+
|
|
217
|
+
def delete_mcp(self, name: str) -> Dict:
|
|
218
|
+
"""Remove an MCP server from config and disconnect it."""
|
|
219
|
+
return self._request("DELETE", f"/api/mcp/{name}")
|
|
220
|
+
|
|
221
|
+
def enable_mcp(self, name: str) -> Dict:
|
|
222
|
+
"""Enable a disabled MCP server and connect it."""
|
|
223
|
+
return self._request("POST", f"/api/mcp/{name}/enable")
|
|
224
|
+
|
|
225
|
+
def disable_mcp(self, name: str) -> Dict:
|
|
226
|
+
"""Disable an MCP server (keeps config, stops connection)."""
|
|
227
|
+
return self._request("POST", f"/api/mcp/{name}/disable")
|
|
228
|
+
|
|
229
|
+
def reload_mcp(self) -> Dict:
|
|
230
|
+
"""Disconnect all MCP servers and reconnect every enabled one."""
|
|
231
|
+
return self._request("POST", "/api/mcp/reload")
|
|
232
|
+
|
|
233
|
+
# ── Backend management ────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
def stats(self) -> Dict:
|
|
236
|
+
"""Return backend stats: mode, agents, prompts, tools, lines, mcp_count."""
|
|
237
|
+
return self._request("GET", "/api/stats")
|
|
238
|
+
|
|
239
|
+
def skills(self) -> list:
|
|
240
|
+
"""Return the catalogue of all built-in skill agents with active status."""
|
|
241
|
+
return self._request("GET", "/api/skills")
|
|
242
|
+
|
|
243
|
+
def shutdown(self) -> Dict:
|
|
244
|
+
"""Gracefully stop the backend. Returns the last session ID.
|
|
245
|
+
|
|
246
|
+
After this call the backend process exits. Resume with:
|
|
247
|
+
swebot --serve
|
|
248
|
+
swebot --tui (or --session <last_session_id>)
|
|
249
|
+
"""
|
|
250
|
+
return self._request("POST", "/api/shutdown")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class Session:
|
|
254
|
+
"""A single conversation session with the swebot agent."""
|
|
255
|
+
|
|
256
|
+
def __init__(self, client: SwebotClient, session_id: str,
|
|
257
|
+
agent: str = "default", data: dict = None):
|
|
258
|
+
self.client = client
|
|
259
|
+
self.id = session_id
|
|
260
|
+
self.agent = agent
|
|
261
|
+
self._data = data or {}
|
|
262
|
+
|
|
263
|
+
# ── Messaging ─────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
def stream(self, message: str) -> Iterator[str]:
|
|
266
|
+
"""Stream the assistant's reply token-by-token."""
|
|
267
|
+
for ev in self._stream_events(message):
|
|
268
|
+
if ev.kind == "content_delta":
|
|
269
|
+
yield ev.data.get("delta", "")
|
|
270
|
+
|
|
271
|
+
def send(self, message: str) -> str:
|
|
272
|
+
"""Send a message and return the complete reply (non-streaming)."""
|
|
273
|
+
reply = ""
|
|
274
|
+
for ev in self._stream_events(message):
|
|
275
|
+
if ev.kind == "content_delta":
|
|
276
|
+
reply += ev.data.get("delta", "")
|
|
277
|
+
elif ev.kind == "message_complete":
|
|
278
|
+
reply = ev.data.get("content", reply)
|
|
279
|
+
return reply
|
|
280
|
+
|
|
281
|
+
def send_with_events(
|
|
282
|
+
self,
|
|
283
|
+
message: str,
|
|
284
|
+
on_token: Optional[Callable[[str], None]] = None,
|
|
285
|
+
on_tool_start: Optional[Callable[[ToolCall], None]] = None,
|
|
286
|
+
on_tool_result: Optional[Callable[[ToolCall], None]] = None,
|
|
287
|
+
) -> str:
|
|
288
|
+
"""Send a message, firing callbacks for tokens and tool calls."""
|
|
289
|
+
reply = ""
|
|
290
|
+
active_tools: dict[str, ToolCall] = {}
|
|
291
|
+
|
|
292
|
+
for ev in self._stream_events(message):
|
|
293
|
+
if ev.kind == "content_delta":
|
|
294
|
+
delta = ev.data.get("delta", "")
|
|
295
|
+
reply += delta
|
|
296
|
+
if on_token:
|
|
297
|
+
on_token(delta)
|
|
298
|
+
|
|
299
|
+
elif ev.kind == "tool_start":
|
|
300
|
+
tc = ToolCall(
|
|
301
|
+
id=ev.data.get("tool_id", ""),
|
|
302
|
+
name=ev.data.get("tool_name", ""),
|
|
303
|
+
)
|
|
304
|
+
active_tools[tc.id] = tc
|
|
305
|
+
if on_tool_start:
|
|
306
|
+
on_tool_start(tc)
|
|
307
|
+
|
|
308
|
+
elif ev.kind == "tool_result":
|
|
309
|
+
tid = ev.data.get("tool_id", "")
|
|
310
|
+
tc = active_tools.pop(tid, ToolCall(id=tid, name=ev.data.get("tool_name", "")))
|
|
311
|
+
tc.output = ev.data.get("content", "")
|
|
312
|
+
tc.is_error = ev.data.get("is_error", False)
|
|
313
|
+
if on_tool_result:
|
|
314
|
+
on_tool_result(tc)
|
|
315
|
+
|
|
316
|
+
elif ev.kind == "message_complete":
|
|
317
|
+
reply = ev.data.get("content", reply)
|
|
318
|
+
|
|
319
|
+
return reply
|
|
320
|
+
|
|
321
|
+
def _stream_events(self, message: str) -> Iterator[Event]:
|
|
322
|
+
return self.client._stream(
|
|
323
|
+
f"/api/sessions/{self.id}/messages",
|
|
324
|
+
{"content": message},
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# ── Todos ─────────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
def compact(self) -> Dict:
|
|
330
|
+
"""Manually trigger context compaction for this session.
|
|
331
|
+
|
|
332
|
+
swebot auto-compacts at 85 % of the model context window. Call this
|
|
333
|
+
to compact earlier and free up tokens.
|
|
334
|
+
|
|
335
|
+
Returns a dict with ``{"compacted": true, "summary": "..."}`` on
|
|
336
|
+
success or ``{"compacted": false, "reason": "..."}`` when not needed.
|
|
337
|
+
"""
|
|
338
|
+
return self.client._request("POST", f"/api/sessions/{self.id}/compact")
|
|
339
|
+
|
|
340
|
+
def clear(self) -> Dict:
|
|
341
|
+
"""Clear all context (messages + knowledge) from this session.
|
|
342
|
+
|
|
343
|
+
Resets the session to a fresh state as if swebot just started, without
|
|
344
|
+
restarting the process. The session ID, agent, and model are preserved.
|
|
345
|
+
|
|
346
|
+
Useful for:
|
|
347
|
+
- SWE-bench harness: clean slate between tasks
|
|
348
|
+
- Switching to a different task without restarting
|
|
349
|
+
|
|
350
|
+
Returns ``{"status": "cleared"}`` on success.
|
|
351
|
+
"""
|
|
352
|
+
return self.client._request("POST", f"/api/sessions/{self.id}/clear")
|
|
353
|
+
|
|
354
|
+
def token_usage(self) -> Dict:
|
|
355
|
+
"""Return token usage for this session from the stats endpoint.
|
|
356
|
+
|
|
357
|
+
Returns::
|
|
358
|
+
|
|
359
|
+
{
|
|
360
|
+
"tokens_in": 14200,
|
|
361
|
+
"tokens_out": 3400,
|
|
362
|
+
"context_window": 200000,
|
|
363
|
+
"usage_pct": 8.8,
|
|
364
|
+
}
|
|
365
|
+
"""
|
|
366
|
+
stats = self.client._request("GET", "/api/stats")
|
|
367
|
+
win = stats.get("context_window", 0)
|
|
368
|
+
tin = stats.get("tokens_in", 0)
|
|
369
|
+
tout = stats.get("tokens_out", 0)
|
|
370
|
+
pct = round((tin / win * 100), 1) if win else 0
|
|
371
|
+
return {
|
|
372
|
+
"tokens_in": tin,
|
|
373
|
+
"tokens_out": tout,
|
|
374
|
+
"context_window": win,
|
|
375
|
+
"usage_pct": pct,
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
def get_todos(self) -> List['TodoItem']:
|
|
379
|
+
resp = self.client._request("GET", f"/api/sessions/{self.id}/todos")
|
|
380
|
+
todos_list = resp if isinstance(resp, list) else resp.get("todos", [])
|
|
381
|
+
return [
|
|
382
|
+
TodoItem(
|
|
383
|
+
id=t["id"], title=t["title"],
|
|
384
|
+
description=t.get("description", ""),
|
|
385
|
+
status=t.get("status", "pending"),
|
|
386
|
+
)
|
|
387
|
+
for t in todos_list
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
def update_todo(self, todo_id: str, status: str) -> None:
|
|
391
|
+
self.client._request(
|
|
392
|
+
"PATCH", f"/api/sessions/{self.id}/todos/{todo_id}",
|
|
393
|
+
{"status": status},
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# ── Info ──────────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
def refresh(self) -> "Session":
|
|
399
|
+
self._data = self.client._request("GET", f"/api/sessions/{self.id}")
|
|
400
|
+
return self
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def message_count(self) -> int:
|
|
404
|
+
return self._data.get("message_count", 0)
|
|
405
|
+
|
|
406
|
+
def __repr__(self) -> str:
|
|
407
|
+
return f"Session(id={self.id!r}, agent={self.agent!r})"
|