cellcog 2.3.0__tar.gz → 2.4.0__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.
- {cellcog-2.3.0 → cellcog-2.4.0}/PKG-INFO +57 -6
- {cellcog-2.3.0 → cellcog-2.4.0}/README.md +56 -5
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/__init__.py +1 -1
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/chat.py +23 -1
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/client.py +237 -32
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/daemon/main.py +5 -4
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/daemon/state.py +3 -1
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/docs/api_reference.md +76 -7
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/files.py +2 -1
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/message_processor.py +2 -1
- cellcog-2.4.0/cellcog/state_paths.py +83 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/pyproject.toml +1 -1
- {cellcog-2.3.0 → cellcog-2.4.0}/.convex/.gitignore +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/.convex/local/default/config.json +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/.convex/local/default/convex_local_backend.sqlite3 +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/.convex/local/default/convex_local_storage/modules/18330a2b-2cf5-4f51-89a5-8d97a422e7a2.blob +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/.gitignore +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/LICENSE +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/auth.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/config.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/daemon/__init__.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/daemon/delivery.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/docs/__init__.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/docs/support.md +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/exceptions.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/message_formatter.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/cellcog/version_detection.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/examples/basic_usage.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/examples/file_handling.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/examples/streaming_usage.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/examples/sync_usage.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/examples/workflow_usage.py +0 -0
- {cellcog-2.3.0 → cellcog-2.4.0}/tests/test_gs_url_handling.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cellcog
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Python SDK for CellCog - Any-to-Any AI for agents. Fire-and-forget pattern with WebSocket notifications.
|
|
5
5
|
Project-URL: Homepage, https://cellcog.ai
|
|
6
6
|
Project-URL: Documentation, https://cellcog.ai/developer/docs
|
|
@@ -78,11 +78,13 @@ result = client.create_chat(
|
|
|
78
78
|
|
|
79
79
|
## How It Works
|
|
80
80
|
|
|
81
|
-
**
|
|
81
|
+
**Three delivery modes:**
|
|
82
82
|
|
|
83
83
|
- **Wait for Completion (default)** — Blocks until CellCog finishes and returns the full result. Works with any agent — OpenClaw, Claude Code, Cursor, or any Python environment.
|
|
84
84
|
|
|
85
|
-
- **
|
|
85
|
+
- **Send-only (`delivery="send_only"`)** — Fire-and-forget for ANY agent: returns the moment the chat/message is accepted (`status="accepted"`). Poll `get_status()` and fetch results with `wait_for_completion()`/`get_history()`. Use this when orchestrating several chats in parallel — the blocking default would hang on each chat's whole run. Works on `create_chat()` AND `send_message()`.
|
|
86
|
+
|
|
87
|
+
- **Notify on Completion (OpenClaw)** — Returns immediately. A background daemon monitors via WebSocket and delivers results to your OpenClaw session when done. Requires `sessions_send` on OpenClaw Gateway.
|
|
86
88
|
|
|
87
89
|
All methods return the same unified shape:
|
|
88
90
|
|
|
@@ -90,15 +92,23 @@ All methods return the same unified shape:
|
|
|
90
92
|
{
|
|
91
93
|
"chat_id": str,
|
|
92
94
|
"is_operating": bool,
|
|
93
|
-
"status": str, # "completed" | "tracking" | "
|
|
95
|
+
"status": str, # "completed" | "tracking" | "accepted" | "operating"
|
|
94
96
|
"message": str, # Always print this in full
|
|
95
97
|
}
|
|
96
98
|
```
|
|
97
99
|
|
|
100
|
+
A wait that gives up returns `status="operating"` with `timed_out=True` — the chat is still
|
|
101
|
+
running server-side; resume with `wait_for_completion()`.
|
|
102
|
+
|
|
98
103
|
## Configuration
|
|
99
104
|
|
|
100
105
|
```bash
|
|
101
106
|
export CELLCOG_API_KEY="sk_..."
|
|
107
|
+
|
|
108
|
+
# Optional: where SDK state lives (tracked chats, downloads, daemon files).
|
|
109
|
+
# Default ~/.cellcog; in headless/container environments where HOME isn't
|
|
110
|
+
# writable, the SDK falls back to a temp directory automatically.
|
|
111
|
+
export CELLCOG_STATE_DIR="/path/to/state"
|
|
102
112
|
```
|
|
103
113
|
|
|
104
114
|
**Get your API key:**
|
|
@@ -148,13 +158,48 @@ result = client.create_chat(
|
|
|
148
158
|
prompt="...",
|
|
149
159
|
task_label="...",
|
|
150
160
|
chat_mode="agent",
|
|
161
|
+
chat_tier="max", # "flash" | "core" | "max" — omit for the SDK default
|
|
162
|
+
delivery="send_only", # fire-and-forget; default blocks until done
|
|
151
163
|
project_id="...", # CellCog project for document context
|
|
152
164
|
agent_role_id="...", # Specialized agent role
|
|
153
165
|
enable_cowork=True, # Direct machine access via CellCog Desktop
|
|
154
166
|
cowork_working_directory="/Users/...", # Working directory for co-work
|
|
167
|
+
enable_browse=True, # Drive the user's REAL Chrome (needs Desktop + extension)
|
|
168
|
+
browser_profile_id="Default", # from client.get_browser_status()
|
|
169
|
+
enable_tools=True, # the user's connected SaaS tools
|
|
170
|
+
tools_selection=["gmail", "notion"], # toolkit slugs (omit for ALL); from client.list_toolkits()
|
|
155
171
|
)
|
|
156
172
|
```
|
|
157
173
|
|
|
174
|
+
## Browse & Tools
|
|
175
|
+
|
|
176
|
+
CellCog chats can drive the user's **real Chrome** (Browse) and call their **connected
|
|
177
|
+
SaaS tools** (Gmail, Notion, Slack, ... — Tools). Discover what's available, then enable
|
|
178
|
+
at chat creation:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
# Browse: discover Chrome profiles, then enable with one
|
|
182
|
+
status = client.get_browser_status()
|
|
183
|
+
profile = status["active_profile"] # or let the user pick from available_profiles
|
|
184
|
+
result = client.create_chat(
|
|
185
|
+
prompt="Log into the dashboard and screenshot the weekly report",
|
|
186
|
+
enable_browse=True,
|
|
187
|
+
browser_profile_id=profile["profileDir"],
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Tools: discover connected toolkits, then enable a selection (omit for ALL)
|
|
191
|
+
toolkits = client.list_toolkits(connected_only=True)
|
|
192
|
+
result = client.create_chat(
|
|
193
|
+
prompt="Summarize this week's unread emails",
|
|
194
|
+
enable_tools=True,
|
|
195
|
+
tools_selection=["gmail"], # toolkit slugs
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Notes: Browse requires CellCog Desktop + the Chrome extension on the user's machine and
|
|
200
|
+
auto-enables co-work server-side. `tools_selection` takes TOOLKIT slugs (the granularity
|
|
201
|
+
users pick in the product UI); `list_toolkit_tools(slug)` shows what a toolkit unlocks.
|
|
202
|
+
|
|
158
203
|
## File Handling
|
|
159
204
|
|
|
160
205
|
```python
|
|
@@ -180,9 +225,15 @@ a tier for the DEPTH (omit `chat_tier` for the mode's default).
|
|
|
180
225
|
|
|
181
226
|
| Mode | Best For | Tiers |
|
|
182
227
|
|------|----------|-------|
|
|
183
|
-
| `"agent"` | Most tasks —
|
|
228
|
+
| `"agent"` | Most tasks — assets, documents, production pipelines, coding/co-work | `flash` (SDK default) · `core` · `max` |
|
|
184
229
|
| `"creative"` | Design, brand, frontend craft — taste-first work | `core` (default) · `max` |
|
|
185
|
-
| `"team"` | Deep multi-
|
|
230
|
+
| `"team"` | Deep research ONLY — multi-source synthesis, citations (multi-agent) | `flash` · `core` (default) · `max` |
|
|
231
|
+
|
|
232
|
+
**Tier guidance for agent mode:** omit `chat_tier` and the SDK sends `"flash"` — right for
|
|
233
|
+
simple asset generation and light tasks. **Coding / co-work needs `"max"`** (applied
|
|
234
|
+
automatically when `enable_cowork=True`). Heavy multi-step production (video, data
|
|
235
|
+
analysis, financial models, legal drafting) → pass `chat_tier="max"` explicitly. Quality
|
|
236
|
+
disappointing on flash? Re-run with `chat_tier="max"`. Use `"team"` only for deep research.
|
|
186
237
|
|
|
187
238
|
Legacy mode names (`"agent core"`, `"agent team"`, `"agent team max"`) keep working
|
|
188
239
|
forever — the server normalizes them to their historical operating points.
|
|
@@ -46,11 +46,13 @@ result = client.create_chat(
|
|
|
46
46
|
|
|
47
47
|
## How It Works
|
|
48
48
|
|
|
49
|
-
**
|
|
49
|
+
**Three delivery modes:**
|
|
50
50
|
|
|
51
51
|
- **Wait for Completion (default)** — Blocks until CellCog finishes and returns the full result. Works with any agent — OpenClaw, Claude Code, Cursor, or any Python environment.
|
|
52
52
|
|
|
53
|
-
- **
|
|
53
|
+
- **Send-only (`delivery="send_only"`)** — Fire-and-forget for ANY agent: returns the moment the chat/message is accepted (`status="accepted"`). Poll `get_status()` and fetch results with `wait_for_completion()`/`get_history()`. Use this when orchestrating several chats in parallel — the blocking default would hang on each chat's whole run. Works on `create_chat()` AND `send_message()`.
|
|
54
|
+
|
|
55
|
+
- **Notify on Completion (OpenClaw)** — Returns immediately. A background daemon monitors via WebSocket and delivers results to your OpenClaw session when done. Requires `sessions_send` on OpenClaw Gateway.
|
|
54
56
|
|
|
55
57
|
All methods return the same unified shape:
|
|
56
58
|
|
|
@@ -58,15 +60,23 @@ All methods return the same unified shape:
|
|
|
58
60
|
{
|
|
59
61
|
"chat_id": str,
|
|
60
62
|
"is_operating": bool,
|
|
61
|
-
"status": str, # "completed" | "tracking" | "
|
|
63
|
+
"status": str, # "completed" | "tracking" | "accepted" | "operating"
|
|
62
64
|
"message": str, # Always print this in full
|
|
63
65
|
}
|
|
64
66
|
```
|
|
65
67
|
|
|
68
|
+
A wait that gives up returns `status="operating"` with `timed_out=True` — the chat is still
|
|
69
|
+
running server-side; resume with `wait_for_completion()`.
|
|
70
|
+
|
|
66
71
|
## Configuration
|
|
67
72
|
|
|
68
73
|
```bash
|
|
69
74
|
export CELLCOG_API_KEY="sk_..."
|
|
75
|
+
|
|
76
|
+
# Optional: where SDK state lives (tracked chats, downloads, daemon files).
|
|
77
|
+
# Default ~/.cellcog; in headless/container environments where HOME isn't
|
|
78
|
+
# writable, the SDK falls back to a temp directory automatically.
|
|
79
|
+
export CELLCOG_STATE_DIR="/path/to/state"
|
|
70
80
|
```
|
|
71
81
|
|
|
72
82
|
**Get your API key:**
|
|
@@ -116,13 +126,48 @@ result = client.create_chat(
|
|
|
116
126
|
prompt="...",
|
|
117
127
|
task_label="...",
|
|
118
128
|
chat_mode="agent",
|
|
129
|
+
chat_tier="max", # "flash" | "core" | "max" — omit for the SDK default
|
|
130
|
+
delivery="send_only", # fire-and-forget; default blocks until done
|
|
119
131
|
project_id="...", # CellCog project for document context
|
|
120
132
|
agent_role_id="...", # Specialized agent role
|
|
121
133
|
enable_cowork=True, # Direct machine access via CellCog Desktop
|
|
122
134
|
cowork_working_directory="/Users/...", # Working directory for co-work
|
|
135
|
+
enable_browse=True, # Drive the user's REAL Chrome (needs Desktop + extension)
|
|
136
|
+
browser_profile_id="Default", # from client.get_browser_status()
|
|
137
|
+
enable_tools=True, # the user's connected SaaS tools
|
|
138
|
+
tools_selection=["gmail", "notion"], # toolkit slugs (omit for ALL); from client.list_toolkits()
|
|
123
139
|
)
|
|
124
140
|
```
|
|
125
141
|
|
|
142
|
+
## Browse & Tools
|
|
143
|
+
|
|
144
|
+
CellCog chats can drive the user's **real Chrome** (Browse) and call their **connected
|
|
145
|
+
SaaS tools** (Gmail, Notion, Slack, ... — Tools). Discover what's available, then enable
|
|
146
|
+
at chat creation:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
# Browse: discover Chrome profiles, then enable with one
|
|
150
|
+
status = client.get_browser_status()
|
|
151
|
+
profile = status["active_profile"] # or let the user pick from available_profiles
|
|
152
|
+
result = client.create_chat(
|
|
153
|
+
prompt="Log into the dashboard and screenshot the weekly report",
|
|
154
|
+
enable_browse=True,
|
|
155
|
+
browser_profile_id=profile["profileDir"],
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Tools: discover connected toolkits, then enable a selection (omit for ALL)
|
|
159
|
+
toolkits = client.list_toolkits(connected_only=True)
|
|
160
|
+
result = client.create_chat(
|
|
161
|
+
prompt="Summarize this week's unread emails",
|
|
162
|
+
enable_tools=True,
|
|
163
|
+
tools_selection=["gmail"], # toolkit slugs
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Notes: Browse requires CellCog Desktop + the Chrome extension on the user's machine and
|
|
168
|
+
auto-enables co-work server-side. `tools_selection` takes TOOLKIT slugs (the granularity
|
|
169
|
+
users pick in the product UI); `list_toolkit_tools(slug)` shows what a toolkit unlocks.
|
|
170
|
+
|
|
126
171
|
## File Handling
|
|
127
172
|
|
|
128
173
|
```python
|
|
@@ -148,9 +193,15 @@ a tier for the DEPTH (omit `chat_tier` for the mode's default).
|
|
|
148
193
|
|
|
149
194
|
| Mode | Best For | Tiers |
|
|
150
195
|
|------|----------|-------|
|
|
151
|
-
| `"agent"` | Most tasks —
|
|
196
|
+
| `"agent"` | Most tasks — assets, documents, production pipelines, coding/co-work | `flash` (SDK default) · `core` · `max` |
|
|
152
197
|
| `"creative"` | Design, brand, frontend craft — taste-first work | `core` (default) · `max` |
|
|
153
|
-
| `"team"` | Deep multi-
|
|
198
|
+
| `"team"` | Deep research ONLY — multi-source synthesis, citations (multi-agent) | `flash` · `core` (default) · `max` |
|
|
199
|
+
|
|
200
|
+
**Tier guidance for agent mode:** omit `chat_tier` and the SDK sends `"flash"` — right for
|
|
201
|
+
simple asset generation and light tasks. **Coding / co-work needs `"max"`** (applied
|
|
202
|
+
automatically when `enable_cowork=True`). Heavy multi-step production (video, data
|
|
203
|
+
analysis, financial models, legal drafting) → pass `chat_tier="max"` explicitly. Quality
|
|
204
|
+
disappointing on flash? Re-run with `chat_tier="max"`. Use `"team"` only for deep research.
|
|
154
205
|
|
|
155
206
|
Legacy mode names (`"agent core"`, `"agent team"`, `"agent team max"`) keep working
|
|
156
207
|
forever — the server normalizes them to their historical operating points.
|
|
@@ -19,6 +19,7 @@ from .exceptions import (
|
|
|
19
19
|
PaymentRequiredError,
|
|
20
20
|
)
|
|
21
21
|
from .files import FileProcessor
|
|
22
|
+
from .state_paths import state_path
|
|
22
23
|
|
|
23
24
|
# ── Mode × Tier resolution (Mode × Tier redesign, 2026-08) ──────────────────
|
|
24
25
|
# Friendly SDK names -> API chat_mode values. The server accepts BOTH current
|
|
@@ -80,7 +81,7 @@ class ChatManager:
|
|
|
80
81
|
def __init__(self, config: Config, file_processor: FileProcessor):
|
|
81
82
|
self.config = config
|
|
82
83
|
self.files = file_processor
|
|
83
|
-
self._chats_dir =
|
|
84
|
+
self._chats_dir = state_path("chats")
|
|
84
85
|
|
|
85
86
|
def create(
|
|
86
87
|
self,
|
|
@@ -91,6 +92,10 @@ class ChatManager:
|
|
|
91
92
|
hc_enabled: bool = False,
|
|
92
93
|
hc_working_directory: Optional[str] = None,
|
|
93
94
|
chat_tier: Optional[str] = None,
|
|
95
|
+
browser_enabled: bool = False,
|
|
96
|
+
browser_profile_id: Optional[str] = None,
|
|
97
|
+
personal_tools_enabled: bool = False,
|
|
98
|
+
personal_tools_selection: Optional[list] = None,
|
|
94
99
|
) -> dict:
|
|
95
100
|
"""
|
|
96
101
|
Create a new CellCog chat (without streaming).
|
|
@@ -107,6 +112,13 @@ class ChatManager:
|
|
|
107
112
|
agent_role_id: Optional agent role ID within the project. Requires project_id.
|
|
108
113
|
hc_enabled: Enable Human Computer (co-work) for this chat
|
|
109
114
|
hc_working_directory: Working directory on user's machine for HC commands
|
|
115
|
+
browser_enabled: Enable Browse (the user's real Chrome). Server-side this
|
|
116
|
+
auto-enables co-work (Browse rides the Desktop connection).
|
|
117
|
+
browser_profile_id: Chrome profile dir (from the browser-status endpoint's
|
|
118
|
+
available_profiles[].profileDir). None -> "Default".
|
|
119
|
+
personal_tools_enabled: Enable the user's connected SaaS tools for this chat
|
|
120
|
+
personal_tools_selection: Toolkit slugs to restrict the chat to (e.g.
|
|
121
|
+
["gmail", "notion"]). None -> ALL connected tools.
|
|
110
122
|
|
|
111
123
|
Returns:
|
|
112
124
|
{
|
|
@@ -133,6 +145,13 @@ class ChatManager:
|
|
|
133
145
|
data["hc_enabled"] = True
|
|
134
146
|
if hc_working_directory:
|
|
135
147
|
data["hc_working_directory"] = hc_working_directory
|
|
148
|
+
if browser_enabled:
|
|
149
|
+
data["browser_enabled"] = True
|
|
150
|
+
data["browser_profile_id"] = browser_profile_id or "Default"
|
|
151
|
+
if personal_tools_enabled:
|
|
152
|
+
data["personal_tools_enabled"] = True
|
|
153
|
+
if personal_tools_selection is not None:
|
|
154
|
+
data["personal_tools_selection"] = [str(s).strip().lower() for s in personal_tools_selection]
|
|
136
155
|
|
|
137
156
|
resp = self._request("POST", "/cellcog/chat/new", data)
|
|
138
157
|
|
|
@@ -192,6 +211,9 @@ class ChatManager:
|
|
|
192
211
|
"name": resp["name"],
|
|
193
212
|
"is_operating": resp["operating"],
|
|
194
213
|
"error_type": error_type,
|
|
214
|
+
# Most recent agent progress one-liner (server: ChatMeta.latest_status_update,
|
|
215
|
+
# CEL-1300 item 6). None until the agent posts its first update of a run.
|
|
216
|
+
"latest_update": resp.get("latest_status_update"),
|
|
195
217
|
}
|
|
196
218
|
|
|
197
219
|
def get_history(self, chat_id: str, session_id: str) -> dict:
|
|
@@ -22,11 +22,12 @@ from typing import Optional
|
|
|
22
22
|
import requests as _requests
|
|
23
23
|
|
|
24
24
|
from .auth import AuthManager
|
|
25
|
-
from .chat import ChatManager
|
|
25
|
+
from .chat import ChatManager, resolve_chat_mode
|
|
26
26
|
from .config import Config
|
|
27
27
|
from .exceptions import APIError, GatewayConfigError
|
|
28
28
|
from .files import FileProcessor
|
|
29
29
|
from .message_processor import MessageProcessor
|
|
30
|
+
from .state_paths import state_dir, state_path
|
|
30
31
|
from .daemon.state import TrackedChat, Listener, StateManager
|
|
31
32
|
|
|
32
33
|
|
|
@@ -140,7 +141,7 @@ class CellCogClient:
|
|
|
140
141
|
self._state = StateManager()
|
|
141
142
|
|
|
142
143
|
# Daemon management
|
|
143
|
-
self._daemon_pid_file =
|
|
144
|
+
self._daemon_pid_file = state_path("daemon.pid")
|
|
144
145
|
|
|
145
146
|
# Sessions send pre-flight check cache
|
|
146
147
|
# Maps gateway_url -> (is_available: bool, checked_at: float)
|
|
@@ -280,13 +281,20 @@ class CellCogClient:
|
|
|
280
281
|
enable_cowork: bool = False,
|
|
281
282
|
cowork_working_directory: Optional[str] = None,
|
|
282
283
|
chat_tier: Optional[str] = None,
|
|
284
|
+
enable_browse: bool = False,
|
|
285
|
+
browser_profile_id: Optional[str] = None,
|
|
286
|
+
enable_tools: bool = False,
|
|
287
|
+
tools_selection: Optional[list] = None,
|
|
283
288
|
) -> dict:
|
|
284
289
|
"""
|
|
285
290
|
Create a CellCog chat.
|
|
286
291
|
|
|
287
|
-
|
|
292
|
+
Three delivery modes:
|
|
288
293
|
- "wait_for_completion" (default): Blocks until done. Returns full results.
|
|
289
294
|
Works with ANY agent (OpenClaw, Cursor, Claude Code, etc.).
|
|
295
|
+
- "send_only": Fire-and-forget. Returns immediately with status="accepted";
|
|
296
|
+
no daemon needed. Poll get_status()/wait_for_completion() yourself.
|
|
297
|
+
Best for orchestrating several chats in parallel.
|
|
290
298
|
- "notify_on_completion": Returns immediately. Daemon delivers results
|
|
291
299
|
to your OpenClaw session via sessions_send. Requires notify_session_key.
|
|
292
300
|
|
|
@@ -296,8 +304,10 @@ class CellCogClient:
|
|
|
296
304
|
e.g., "agent:main:main"). If provided without explicit delivery param,
|
|
297
305
|
automatically uses notify_on_completion for backward compatibility.
|
|
298
306
|
task_label: Human-readable label (appears in notification)
|
|
299
|
-
delivery: "wait_for_completion" (default, universal)
|
|
300
|
-
"
|
|
307
|
+
delivery: "wait_for_completion" (default, universal, blocking),
|
|
308
|
+
"send_only" (fire-and-forget: returns once the chat is accepted;
|
|
309
|
+
poll with get_status/wait_for_completion), or
|
|
310
|
+
"notify_on_completion" (OpenClaw sessions_send delivery)
|
|
301
311
|
timeout: Max seconds to wait (wait_for_completion mode only, default 1800)
|
|
302
312
|
gateway_url: OpenClaw Gateway URL (notify_on_completion only)
|
|
303
313
|
project_id: Optional CellCog project ID for document context
|
|
@@ -305,9 +315,21 @@ class CellCogClient:
|
|
|
305
315
|
chat_mode: "agent", "creative", or "team". Legacy names — "agent core",
|
|
306
316
|
"agent team", "agent team max" — keep working forever.
|
|
307
317
|
chat_tier: "flash" | "core" | "max" — depth/spend within the mode.
|
|
308
|
-
None
|
|
318
|
+
None in agent mode -> SDK default: "flash" (or "max" when
|
|
319
|
+
enable_cowork=True — coding needs depth). Other modes: the
|
|
320
|
+
server applies the mode's default. "creative" has no "flash" tier.
|
|
309
321
|
enable_cowork: Enable co-work on user's PC
|
|
310
322
|
cowork_working_directory: Working directory for co-work commands
|
|
323
|
+
enable_browse: Enable Browse — the agent drives the user's REAL Chrome
|
|
324
|
+
(requires CellCog Desktop + the Chrome extension). Auto-enables
|
|
325
|
+
co-work server-side. Discover profiles via get_browser_status().
|
|
326
|
+
browser_profile_id: Chrome profile to browse with — a profileDir from
|
|
327
|
+
get_browser_status()["available_profiles"]. None -> "Default".
|
|
328
|
+
enable_tools: Enable the user's connected SaaS tools (Gmail, Notion, ...)
|
|
329
|
+
for this chat. Discover toolkits via list_toolkits().
|
|
330
|
+
tools_selection: Restrict the chat to these toolkit slugs (e.g.
|
|
331
|
+
["gmail", "notion"]). None -> ALL connected tools. no_auth built-in
|
|
332
|
+
toolkits work regardless of this selection.
|
|
311
333
|
|
|
312
334
|
Returns:
|
|
313
335
|
All modes return: {"chat_id": str, "is_operating": bool, "status": str, "message": str}
|
|
@@ -347,10 +369,11 @@ class CellCogClient:
|
|
|
347
369
|
if notify_session_key and delivery == "wait_for_completion":
|
|
348
370
|
delivery = "notify_on_completion"
|
|
349
371
|
|
|
350
|
-
if delivery not in ("notify_on_completion", "wait_for_completion"):
|
|
372
|
+
if delivery not in ("notify_on_completion", "wait_for_completion", "send_only"):
|
|
351
373
|
raise ValueError(
|
|
352
374
|
f"Invalid delivery mode: '{delivery}'. "
|
|
353
|
-
"Use '
|
|
375
|
+
"Use 'wait_for_completion' (universal, blocking), 'send_only' "
|
|
376
|
+
"(fire-and-forget, poll later), or 'notify_on_completion' (OpenClaw)."
|
|
354
377
|
)
|
|
355
378
|
|
|
356
379
|
if delivery == "notify_on_completion" and not notify_session_key:
|
|
@@ -370,6 +393,49 @@ class CellCogClient:
|
|
|
370
393
|
if delivery == "notify_on_completion":
|
|
371
394
|
self._require_sessions_send(gateway_url)
|
|
372
395
|
|
|
396
|
+
# ── Tools selection validation (toolkit slugs only) ──
|
|
397
|
+
if tools_selection is not None:
|
|
398
|
+
if not enable_tools:
|
|
399
|
+
raise ValueError("tools_selection requires enable_tools=True")
|
|
400
|
+
if not isinstance(tools_selection, (list, tuple)) or not all(
|
|
401
|
+
isinstance(s, str) and s.strip() for s in tools_selection
|
|
402
|
+
):
|
|
403
|
+
raise ValueError(
|
|
404
|
+
"tools_selection must be a list of toolkit slugs (strings), e.g. "
|
|
405
|
+
"['gmail', 'notion'] — discover slugs via client.list_toolkits()."
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# ── Browse pre-flight: fail fast BEFORE creating (and billing) a chat whose
|
|
409
|
+
# browser commands could never run. Best-effort — a status-fetch error never blocks.
|
|
410
|
+
if enable_browse:
|
|
411
|
+
try:
|
|
412
|
+
_bstatus = self.get_browser_status()
|
|
413
|
+
except Exception:
|
|
414
|
+
_bstatus = None
|
|
415
|
+
if _bstatus is not None:
|
|
416
|
+
_profiles = _bstatus.get("available_profiles") or []
|
|
417
|
+
if not any(p.get("hasExtension") for p in _profiles):
|
|
418
|
+
raise ValueError(
|
|
419
|
+
"enable_browse=True but no Chrome profile has the CellCog extension "
|
|
420
|
+
"(or CellCog Desktop is not connected). The user must connect Desktop "
|
|
421
|
+
"and install the extension first — check client.get_browser_status()."
|
|
422
|
+
)
|
|
423
|
+
if browser_profile_id and not any(
|
|
424
|
+
p.get("profileDir") == browser_profile_id for p in _profiles
|
|
425
|
+
):
|
|
426
|
+
_known = ", ".join(sorted(str(p.get("profileDir")) for p in _profiles))
|
|
427
|
+
raise ValueError(
|
|
428
|
+
f"browser_profile_id {browser_profile_id!r} not found in the user's "
|
|
429
|
+
f"Chrome. Known profiles: {_known}. Pick a profileDir from "
|
|
430
|
+
"client.get_browser_status()['available_profiles']."
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# ── SDK tier default (Mode × Tier rollout, 2026-08): when no chat_tier is
|
|
434
|
+
# given in agent mode, default to the LOWEST tier — "flash" — except co-work
|
|
435
|
+
# (coding on a real machine), which needs "max". Explicit chat_tier always wins.
|
|
436
|
+
if chat_tier is None and resolve_chat_mode(chat_mode) == "human_in_the_loop":
|
|
437
|
+
chat_tier = "max" if enable_cowork else "flash"
|
|
438
|
+
|
|
373
439
|
# ── Create the chat via API ──
|
|
374
440
|
api_result = self._chat.create(
|
|
375
441
|
prompt, project_id, chat_mode,
|
|
@@ -377,10 +443,28 @@ class CellCogClient:
|
|
|
377
443
|
agent_role_id=agent_role_id,
|
|
378
444
|
hc_enabled=enable_cowork,
|
|
379
445
|
hc_working_directory=cowork_working_directory,
|
|
446
|
+
browser_enabled=enable_browse,
|
|
447
|
+
browser_profile_id=browser_profile_id,
|
|
448
|
+
personal_tools_enabled=enable_tools,
|
|
449
|
+
personal_tools_selection=list(tools_selection) if tools_selection is not None else None,
|
|
380
450
|
)
|
|
381
451
|
chat_id = api_result["chat_id"]
|
|
382
452
|
task_label = task_label or f"chat-{chat_id[:8]}"
|
|
383
453
|
|
|
454
|
+
# ── send_only: fire-and-forget — no tracking, no daemon, return at once ──
|
|
455
|
+
if delivery == "send_only":
|
|
456
|
+
return {
|
|
457
|
+
"chat_id": chat_id,
|
|
458
|
+
"is_operating": True,
|
|
459
|
+
"status": "accepted",
|
|
460
|
+
"message": (
|
|
461
|
+
f"Chat {chat_id} accepted and running on CellCog (fire-and-forget).\n"
|
|
462
|
+
f'Poll progress: client.get_status("{chat_id}")\n'
|
|
463
|
+
f'Block until done: client.wait_for_completion("{chat_id}", timeout=1800)\n'
|
|
464
|
+
f'Fetch results: client.get_history("{chat_id}")'
|
|
465
|
+
),
|
|
466
|
+
}
|
|
467
|
+
|
|
384
468
|
# ── Local setup: tracking + daemon + delivery ──
|
|
385
469
|
# Wrapped in try-except so chat_id is always returned even if local
|
|
386
470
|
# setup fails. The chat was already created server-side and billed —
|
|
@@ -448,6 +532,10 @@ class CellCogClient:
|
|
|
448
532
|
|
|
449
533
|
Same delivery modes as create_chat:
|
|
450
534
|
- "wait_for_completion" (default): Blocks until done. Returns full results.
|
|
535
|
+
- "send_only": Fire-and-forget. Returns immediately after the message is
|
|
536
|
+
accepted; poll get_status()/wait_for_completion() yourself. Use this when
|
|
537
|
+
relaying messages to several chats — the blocking default would hang on
|
|
538
|
+
each chat's entire run.
|
|
451
539
|
- "notify_on_completion": Returns immediately. Daemon delivers later.
|
|
452
540
|
|
|
453
541
|
Args:
|
|
@@ -455,7 +543,8 @@ class CellCogClient:
|
|
|
455
543
|
message: Your message (supports SHOW_FILE tags for file uploads)
|
|
456
544
|
notify_session_key: OpenClaw session key (required for notify_on_completion)
|
|
457
545
|
task_label: Label for notification (default: "continue-{chat_id[:8]}")
|
|
458
|
-
delivery: "wait_for_completion"
|
|
546
|
+
delivery: "wait_for_completion" (blocking, default), "send_only"
|
|
547
|
+
(fire-and-forget), or "notify_on_completion" (OpenClaw)
|
|
459
548
|
timeout: Max seconds to wait (wait_for_completion only, default 1800)
|
|
460
549
|
gateway_url: OpenClaw Gateway URL (notify_on_completion only)
|
|
461
550
|
|
|
@@ -468,10 +557,11 @@ class CellCogClient:
|
|
|
468
557
|
if notify_session_key and delivery == "wait_for_completion":
|
|
469
558
|
delivery = "notify_on_completion"
|
|
470
559
|
|
|
471
|
-
if delivery not in ("notify_on_completion", "wait_for_completion"):
|
|
560
|
+
if delivery not in ("notify_on_completion", "wait_for_completion", "send_only"):
|
|
472
561
|
raise ValueError(
|
|
473
562
|
f"Invalid delivery mode: '{delivery}'. "
|
|
474
|
-
"Use '
|
|
563
|
+
"Use 'wait_for_completion' (universal, blocking), 'send_only' "
|
|
564
|
+
"(fire-and-forget, poll later), or 'notify_on_completion' (OpenClaw)."
|
|
475
565
|
)
|
|
476
566
|
|
|
477
567
|
if delivery == "notify_on_completion" and not notify_session_key:
|
|
@@ -494,6 +584,19 @@ class CellCogClient:
|
|
|
494
584
|
# ── Send message via API ──
|
|
495
585
|
api_result = self._chat.send_message(chat_id, message)
|
|
496
586
|
|
|
587
|
+
# ── send_only: fire-and-forget — message delivered, return at once ──
|
|
588
|
+
if delivery == "send_only":
|
|
589
|
+
return {
|
|
590
|
+
"chat_id": chat_id,
|
|
591
|
+
"is_operating": True,
|
|
592
|
+
"status": "accepted",
|
|
593
|
+
"message": (
|
|
594
|
+
f"Message delivered to chat {chat_id}; CellCog is working (fire-and-forget).\n"
|
|
595
|
+
f'Poll progress: client.get_status("{chat_id}")\n'
|
|
596
|
+
f'Block until done: client.wait_for_completion("{chat_id}", timeout=1800)'
|
|
597
|
+
),
|
|
598
|
+
}
|
|
599
|
+
|
|
497
600
|
# ── Local setup (same protection as create_chat) ──
|
|
498
601
|
try:
|
|
499
602
|
tracking_info = self._track_chat(
|
|
@@ -630,11 +733,80 @@ class CellCogClient:
|
|
|
630
733
|
"status": "processing" | "ready" | "error",
|
|
631
734
|
"name": str,
|
|
632
735
|
"is_operating": bool,
|
|
633
|
-
"error_type": str | None
|
|
736
|
+
"error_type": str | None,
|
|
737
|
+
"latest_update": str | None # most recent agent progress one-liner
|
|
634
738
|
}
|
|
635
739
|
"""
|
|
636
740
|
return self._chat.get_status(chat_id)
|
|
637
741
|
|
|
742
|
+
# ==================== Capability Discovery (Browse & Tools) ====================
|
|
743
|
+
|
|
744
|
+
def get_browser_status(self) -> dict:
|
|
745
|
+
"""
|
|
746
|
+
Browse availability on the user's machine — use this to populate a
|
|
747
|
+
profile picker before create_chat(enable_browse=True).
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
{
|
|
751
|
+
"extension_installed": bool, # CellCog Chrome extension present?
|
|
752
|
+
"chrome_running": bool, # Chrome connected to Desktop?
|
|
753
|
+
"active_profile": dict | None, # sensible default profile
|
|
754
|
+
"available_profiles": [ # one per Chrome profile
|
|
755
|
+
{"profileDir": str, # -> pass as browser_profile_id
|
|
756
|
+
"profileName": str,
|
|
757
|
+
"hostedDomain": str | None,
|
|
758
|
+
"hasExtension": bool, # extension installed in this profile
|
|
759
|
+
"connected": bool}, # currently connected via Desktop
|
|
760
|
+
...
|
|
761
|
+
],
|
|
762
|
+
"agent_tab_count": int,
|
|
763
|
+
}
|
|
764
|
+
"""
|
|
765
|
+
self.config.require_configured()
|
|
766
|
+
return self._chat._request("GET", "/cellcog/human-computer/browser/status")
|
|
767
|
+
|
|
768
|
+
def list_toolkits(self, connected_only: bool = False, search: Optional[str] = None) -> list:
|
|
769
|
+
"""
|
|
770
|
+
The SaaS toolkit catalog with the user's connection state — use this to
|
|
771
|
+
populate a tool picker before create_chat(enable_tools=True).
|
|
772
|
+
|
|
773
|
+
Args:
|
|
774
|
+
connected_only: Only toolkits the user has actually connected —
|
|
775
|
+
the set that matters for tools_selection.
|
|
776
|
+
search: Optional name filter (server-side, case-insensitive).
|
|
777
|
+
|
|
778
|
+
Returns:
|
|
779
|
+
List of toolkit dicts; each has "slug" (-> pass in tools_selection),
|
|
780
|
+
"name", and "connection" (dict when connected, None otherwise).
|
|
781
|
+
"""
|
|
782
|
+
self.config.require_configured()
|
|
783
|
+
from urllib.parse import quote
|
|
784
|
+
|
|
785
|
+
items, offset, limit = [], 0, 200
|
|
786
|
+
while True:
|
|
787
|
+
path = f"/cellcog/personal-tools/toolkits?limit={limit}&offset={offset}"
|
|
788
|
+
if search:
|
|
789
|
+
path += f"&search={quote(search)}"
|
|
790
|
+
resp = self._chat._request("GET", path)
|
|
791
|
+
page = resp.get("items", [])
|
|
792
|
+
items.extend(page)
|
|
793
|
+
total = resp.get("total", len(items))
|
|
794
|
+
offset += limit
|
|
795
|
+
if len(items) >= total or not page or offset > 5000:
|
|
796
|
+
break
|
|
797
|
+
if connected_only:
|
|
798
|
+
items = [t for t in items if t.get("connection")]
|
|
799
|
+
return items
|
|
800
|
+
|
|
801
|
+
def list_toolkit_tools(self, toolkit_slug: str) -> list:
|
|
802
|
+
"""
|
|
803
|
+
The individual tools (action slugs) a toolkit carries — for showing users
|
|
804
|
+
what enabling a toolkit unlocks. (tools_selection itself takes TOOLKIT
|
|
805
|
+
slugs, not tool slugs.)
|
|
806
|
+
"""
|
|
807
|
+
self.config.require_configured()
|
|
808
|
+
return self._chat._request("GET", f"/cellcog/personal-tools/toolkits/{toolkit_slug}/tools")
|
|
809
|
+
|
|
638
810
|
# ==================== Chat Tracking Recovery ====================
|
|
639
811
|
|
|
640
812
|
def restart_chat_tracking(self) -> dict:
|
|
@@ -670,7 +842,7 @@ class CellCogClient:
|
|
|
670
842
|
self._kill_daemon_if_running()
|
|
671
843
|
|
|
672
844
|
# Count tracking files to report
|
|
673
|
-
tracked_dir =
|
|
845
|
+
tracked_dir = state_path("tracked_chats")
|
|
674
846
|
tracked_count = sum(1 for _ in tracked_dir.glob("*.json")) if tracked_dir.exists() else 0
|
|
675
847
|
|
|
676
848
|
if tracked_count == 0:
|
|
@@ -698,7 +870,7 @@ class CellCogClient:
|
|
|
698
870
|
return {
|
|
699
871
|
"status": "failed",
|
|
700
872
|
"tracked_chats": tracked_count,
|
|
701
|
-
"message": "Failed to start daemon. Check
|
|
873
|
+
"message": f"Failed to start daemon. Check {state_path('daemon.log')}"
|
|
702
874
|
}
|
|
703
875
|
|
|
704
876
|
# ==================== Synchronous Wait ====================
|
|
@@ -721,7 +893,8 @@ class CellCogClient:
|
|
|
721
893
|
|
|
722
894
|
On completion: status="completed", message contains full response with
|
|
723
895
|
file paths, credits used, and next steps.
|
|
724
|
-
On timeout: status="
|
|
896
|
+
On timeout: status="operating" with timed_out=True (the chat is STILL
|
|
897
|
+
running server-side); message contains progress updates and
|
|
725
898
|
retry guidance.
|
|
726
899
|
|
|
727
900
|
Examples:
|
|
@@ -1021,7 +1194,7 @@ class CellCogClient:
|
|
|
1021
1194
|
|
|
1022
1195
|
# Read interim updates file written by daemon
|
|
1023
1196
|
try:
|
|
1024
|
-
updates_file =
|
|
1197
|
+
updates_file = state_path("chats") / chat_id / ".interim_updates.json"
|
|
1025
1198
|
if updates_file.exists():
|
|
1026
1199
|
raw = json.loads(updates_file.read_text())
|
|
1027
1200
|
now = time.time()
|
|
@@ -1059,10 +1232,14 @@ class CellCogClient:
|
|
|
1059
1232
|
f"Chat ID: {chat_id}",
|
|
1060
1233
|
])
|
|
1061
1234
|
|
|
1235
|
+
# The chat is STILL RUNNING server-side. status="timeout" historically read
|
|
1236
|
+
# as failure and misfired callers' retry logic (CEL-1300); status now reflects
|
|
1237
|
+
# the chat's real state, and timed_out=True carries "the SDK stopped waiting".
|
|
1062
1238
|
return {
|
|
1063
1239
|
"chat_id": chat_id,
|
|
1064
1240
|
"is_operating": True,
|
|
1065
|
-
"status": "
|
|
1241
|
+
"status": "operating",
|
|
1242
|
+
"timed_out": True,
|
|
1066
1243
|
"message": "\n".join(lines),
|
|
1067
1244
|
}
|
|
1068
1245
|
|
|
@@ -1555,9 +1732,15 @@ class CellCogClient:
|
|
|
1555
1732
|
|
|
1556
1733
|
# ==================== Daemon Management ====================
|
|
1557
1734
|
|
|
1735
|
+
# Process-wide warn-once / give-up flags (class-level by design): a stale
|
|
1736
|
+
# heartbeat or an unstartable daemon would otherwise print on EVERY SDK call
|
|
1737
|
+
# in headless environments (CEL-1300 item 7).
|
|
1738
|
+
_heartbeat_warned = False
|
|
1739
|
+
_daemon_start_failures = 0
|
|
1740
|
+
|
|
1558
1741
|
def _has_tracked_chats(self) -> bool:
|
|
1559
1742
|
"""Check if there are any tracked chats on disk."""
|
|
1560
|
-
tracked_dir =
|
|
1743
|
+
tracked_dir = state_path("tracked_chats")
|
|
1561
1744
|
if not tracked_dir.exists():
|
|
1562
1745
|
return False
|
|
1563
1746
|
return any(tracked_dir.glob("*.json"))
|
|
@@ -1580,11 +1763,14 @@ class CellCogClient:
|
|
|
1580
1763
|
except (ValueError, ProcessLookupError, PermissionError):
|
|
1581
1764
|
pass
|
|
1582
1765
|
finally:
|
|
1583
|
-
# Clean up PID
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1766
|
+
# Clean up PID + daemon health files. Leaving a stale heartbeat behind
|
|
1767
|
+
# made every subsequent SDK call re-warn "heartbeat stale" and re-kill
|
|
1768
|
+
# forever with the same age (CEL-1300 item 7).
|
|
1769
|
+
for stale in (self._daemon_pid_file, state_path("daemon.heartbeat"), state_path("daemon.version")):
|
|
1770
|
+
try:
|
|
1771
|
+
stale.unlink(missing_ok=True)
|
|
1772
|
+
except Exception:
|
|
1773
|
+
pass
|
|
1588
1774
|
|
|
1589
1775
|
return True
|
|
1590
1776
|
|
|
@@ -1616,7 +1802,18 @@ class CellCogClient:
|
|
|
1616
1802
|
return self._start_daemon()
|
|
1617
1803
|
return True
|
|
1618
1804
|
|
|
1619
|
-
|
|
1805
|
+
if CellCogClient._daemon_start_failures >= 2:
|
|
1806
|
+
return False # daemon can't start in this environment — poll-only mode
|
|
1807
|
+
started = self._start_daemon()
|
|
1808
|
+
if not started:
|
|
1809
|
+
CellCogClient._daemon_start_failures += 1
|
|
1810
|
+
if CellCogClient._daemon_start_failures == 2:
|
|
1811
|
+
print(
|
|
1812
|
+
"Note: CellCog daemon could not be started twice; continuing in "
|
|
1813
|
+
"poll-only mode (create/status/history/wait still work via HTTP).",
|
|
1814
|
+
file=sys.stderr,
|
|
1815
|
+
)
|
|
1816
|
+
return started
|
|
1620
1817
|
finally:
|
|
1621
1818
|
self._daemon_check_in_progress = False
|
|
1622
1819
|
|
|
@@ -1631,7 +1828,7 @@ class CellCogClient:
|
|
|
1631
1828
|
Returns:
|
|
1632
1829
|
True if daemon should be restarted with current SDK
|
|
1633
1830
|
"""
|
|
1634
|
-
version_file =
|
|
1831
|
+
version_file = state_path("daemon.version")
|
|
1635
1832
|
if not version_file.exists():
|
|
1636
1833
|
return True # No version file = old daemon pre-dating this feature
|
|
1637
1834
|
try:
|
|
@@ -1660,17 +1857,20 @@ class CellCogClient:
|
|
|
1660
1857
|
|
|
1661
1858
|
# Heartbeat check: daemon writes timestamp every 30s.
|
|
1662
1859
|
# If stale (>120s), daemon is frozen (e.g., stuck after sleep).
|
|
1663
|
-
heartbeat_file =
|
|
1860
|
+
heartbeat_file = state_path("daemon.heartbeat")
|
|
1664
1861
|
if heartbeat_file.exists():
|
|
1665
1862
|
try:
|
|
1666
1863
|
heartbeat_time = float(heartbeat_file.read_text().strip())
|
|
1667
1864
|
age = time.time() - heartbeat_time
|
|
1668
1865
|
if age > 120:
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1866
|
+
if not CellCogClient._heartbeat_warned:
|
|
1867
|
+
print(
|
|
1868
|
+
f"Warning: Daemon heartbeat stale ({age:.0f}s old). "
|
|
1869
|
+
f"Killing and restarting... (further daemon-health "
|
|
1870
|
+
f"warnings suppressed for this process)",
|
|
1871
|
+
file=sys.stderr
|
|
1872
|
+
)
|
|
1873
|
+
CellCogClient._heartbeat_warned = True
|
|
1674
1874
|
self._kill_daemon_if_running()
|
|
1675
1875
|
return False
|
|
1676
1876
|
except (ValueError, IOError):
|
|
@@ -1697,6 +1897,9 @@ class CellCogClient:
|
|
|
1697
1897
|
self.config.api_key,
|
|
1698
1898
|
self.config.api_base_url
|
|
1699
1899
|
]
|
|
1900
|
+
# Pin the daemon to the SAME state dir the client resolved (matters when
|
|
1901
|
+
# the client fell back to a tmp/override dir — both sides must agree).
|
|
1902
|
+
daemon_env = {**os.environ, "CELLCOG_STATE_DIR": str(state_dir())}
|
|
1700
1903
|
|
|
1701
1904
|
# Start detached process
|
|
1702
1905
|
if sys.platform == "win32":
|
|
@@ -1706,6 +1909,7 @@ class CellCogClient:
|
|
|
1706
1909
|
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
|
|
1707
1910
|
stdout=subprocess.DEVNULL,
|
|
1708
1911
|
stderr=subprocess.DEVNULL,
|
|
1912
|
+
env=daemon_env,
|
|
1709
1913
|
)
|
|
1710
1914
|
else:
|
|
1711
1915
|
# Unix: start new session
|
|
@@ -1714,6 +1918,7 @@ class CellCogClient:
|
|
|
1714
1918
|
start_new_session=True,
|
|
1715
1919
|
stdout=subprocess.DEVNULL,
|
|
1716
1920
|
stderr=subprocess.DEVNULL,
|
|
1921
|
+
env=daemon_env,
|
|
1717
1922
|
)
|
|
1718
1923
|
|
|
1719
1924
|
# Wait briefly for daemon to start
|
|
@@ -1850,7 +2055,7 @@ class CellCogClient:
|
|
|
1850
2055
|
tracking_file.unlink(missing_ok=True)
|
|
1851
2056
|
|
|
1852
2057
|
# Remove seen indices (no longer relevant)
|
|
1853
|
-
seen_dir =
|
|
2058
|
+
seen_dir = state_path("chats", chat_id, ".seen_indices")
|
|
1854
2059
|
if seen_dir.exists():
|
|
1855
2060
|
shutil.rmtree(seen_dir, ignore_errors=True)
|
|
1856
2061
|
|
|
@@ -27,6 +27,7 @@ import requests
|
|
|
27
27
|
|
|
28
28
|
from .delivery import deliver_to_all_listeners, get_gateway_auth, send_to_session
|
|
29
29
|
from .state import Listener, StateManager, TrackedChat
|
|
30
|
+
from ..state_paths import state_path
|
|
30
31
|
|
|
31
32
|
log = logging.getLogger(__name__)
|
|
32
33
|
|
|
@@ -114,7 +115,7 @@ class CellCogDaemon:
|
|
|
114
115
|
|
|
115
116
|
# Resilience: heartbeat file for client-side daemon health checks
|
|
116
117
|
self.heartbeat_task: Optional[asyncio.Task] = None
|
|
117
|
-
self._heartbeat_file =
|
|
118
|
+
self._heartbeat_file = state_path("daemon.heartbeat")
|
|
118
119
|
self._heartbeat_interval = 30 # seconds
|
|
119
120
|
|
|
120
121
|
# Resilience: max tracking age before forced re-verify (24 hours)
|
|
@@ -1335,7 +1336,7 @@ def run_daemon():
|
|
|
1335
1336
|
If no arguments provided, loads from ~/.openclaw/cellcog.json
|
|
1336
1337
|
"""
|
|
1337
1338
|
# Setup logging
|
|
1338
|
-
log_file =
|
|
1339
|
+
log_file = state_path("daemon.log")
|
|
1339
1340
|
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1340
1341
|
|
|
1341
1342
|
logging.basicConfig(
|
|
@@ -1378,7 +1379,7 @@ def run_daemon():
|
|
|
1378
1379
|
loop.add_signal_handler(sig, signal_handler)
|
|
1379
1380
|
|
|
1380
1381
|
# Write PID file
|
|
1381
|
-
pid_file =
|
|
1382
|
+
pid_file = state_path("daemon.pid")
|
|
1382
1383
|
pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1383
1384
|
pid_file.write_text(str(os.getpid()))
|
|
1384
1385
|
log.info(f"Daemon PID: {os.getpid()}")
|
|
@@ -1386,7 +1387,7 @@ def run_daemon():
|
|
|
1386
1387
|
# Write version file for client-side stale daemon detection
|
|
1387
1388
|
from .. import __version__
|
|
1388
1389
|
|
|
1389
|
-
version_file =
|
|
1390
|
+
version_file = state_path("daemon.version")
|
|
1390
1391
|
version_file.write_text(__version__)
|
|
1391
1392
|
log.info(f"Daemon version: {__version__}")
|
|
1392
1393
|
|
|
@@ -11,6 +11,8 @@ from datetime import datetime, timezone
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Optional
|
|
13
13
|
|
|
14
|
+
from ..state_paths import state_dir
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
@dataclass
|
|
16
18
|
class Listener:
|
|
@@ -134,7 +136,7 @@ class StateManager:
|
|
|
134
136
|
Args:
|
|
135
137
|
base_dir: Base directory for state files. Defaults to ~/.cellcog
|
|
136
138
|
"""
|
|
137
|
-
self.base_dir = base_dir or
|
|
139
|
+
self.base_dir = base_dir or state_dir()
|
|
138
140
|
self.tracked_dir = self.base_dir / "tracked_chats"
|
|
139
141
|
self.chats_dir = self.base_dir / "chats"
|
|
140
142
|
|
|
@@ -7,30 +7,49 @@ from cellcog import CellCogClient
|
|
|
7
7
|
client = CellCogClient(agent_provider="openclaw")
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
+
`agent_provider` (required) identifies the agent framework calling CellCog — lowercase
|
|
11
|
+
letters/numbers/hyphens. Common values: `"openclaw"`, `"cursor"`, `"claude-code"`,
|
|
12
|
+
`"codex"`, `"aider"`, `"windsurf"`, `"script"` (standalone scripts), `"cellcog"`
|
|
13
|
+
(CellCog's own agents orchestrating sub-chats).
|
|
14
|
+
|
|
15
|
+
**State directory:** all SDK state (tracked chats, downloads, daemon files) lives under
|
|
16
|
+
`~/.cellcog` by default. Set `CELLCOG_STATE_DIR` to override. In headless/container
|
|
17
|
+
environments where the home directory isn't writable (e.g. `HOME=/`), the SDK falls back
|
|
18
|
+
to a temp directory automatically instead of crashing.
|
|
19
|
+
|
|
10
20
|
---
|
|
11
21
|
|
|
12
22
|
## Chat Operations
|
|
13
23
|
|
|
14
24
|
### create_chat()
|
|
15
25
|
|
|
16
|
-
Create a new CellCog task.
|
|
26
|
+
Create a new CellCog task. Three delivery modes:
|
|
17
27
|
- `wait_for_completion` (default): Blocks until done. Works with any agent.
|
|
28
|
+
- `send_only`: Fire-and-forget. Returns the moment the chat is accepted (`status="accepted"`);
|
|
29
|
+
poll `get_status()` / `wait_for_completion()` yourself. Best for orchestrating several chats.
|
|
18
30
|
- `notify_on_completion`: Returns immediately. OpenClaw only.
|
|
19
31
|
|
|
20
32
|
```python
|
|
21
33
|
result = client.create_chat(
|
|
22
34
|
prompt: str, # Task description (required)
|
|
23
35
|
task_label: str = "", # Human-readable label
|
|
24
|
-
delivery: str = "wait_for_completion",# "wait_for_completion"
|
|
36
|
+
delivery: str = "wait_for_completion",# "wait_for_completion" | "send_only" | "notify_on_completion"
|
|
25
37
|
timeout: int = 1800, # Max wait seconds (wait mode only)
|
|
26
38
|
notify_session_key: str = None, # OpenClaw session key (notify mode only)
|
|
27
39
|
gateway_url: str = None, # OpenClaw Gateway URL (notify mode only)
|
|
28
40
|
chat_mode: str = "agent", # "agent", "creative", "team" (legacy names still work)
|
|
29
|
-
chat_tier: str = None, # "flash" | "core" | "max"
|
|
41
|
+
chat_tier: str = None, # "flash" | "core" | "max". Agent mode + None -> SDK default:
|
|
42
|
+
# "flash" ("max" when enable_cowork=True — coding needs depth).
|
|
43
|
+
# Other modes + None -> server default. Creative has no flash.
|
|
30
44
|
project_id: str = None, # Project context (see project-management-cellcog)
|
|
31
45
|
agent_role_id: str = None, # Specialized role (requires project_id)
|
|
32
46
|
enable_cowork: bool = False, # Direct machine access (see pair-programming-cellcog)
|
|
33
47
|
cowork_working_directory: str = None, # Working dir for co-work
|
|
48
|
+
enable_browse: bool = False, # Drive the user's REAL Chrome (needs Desktop + extension;
|
|
49
|
+
# auto-enables co-work server-side)
|
|
50
|
+
browser_profile_id: str = None, # profileDir from get_browser_status() (None = "Default")
|
|
51
|
+
enable_tools: bool = False, # The user's connected SaaS tools (Gmail, Notion, ...)
|
|
52
|
+
tools_selection: list = None, # Toolkit slugs to restrict to (None = ALL connected tools)
|
|
34
53
|
)
|
|
35
54
|
```
|
|
36
55
|
|
|
@@ -39,10 +58,13 @@ result = client.create_chat(
|
|
|
39
58
|
{
|
|
40
59
|
"chat_id": str, # CellCog chat ID
|
|
41
60
|
"is_operating": bool, # True = still working, False = done
|
|
42
|
-
"status": str, # "completed" | "tracking" | "
|
|
61
|
+
"status": str, # "completed" | "tracking" | "accepted" | "operating"
|
|
43
62
|
"message": str, # THE printable message — always print in full
|
|
44
63
|
}
|
|
45
64
|
```
|
|
65
|
+
A wait that gives up returns `status="operating"` with `timed_out=True` — the chat is
|
|
66
|
+
STILL running server-side; resume with `wait_for_completion()`. `"timeout"` is no longer
|
|
67
|
+
returned (it read as failure and misfired retry logic).
|
|
46
68
|
|
|
47
69
|
**Raises:**
|
|
48
70
|
- `PaymentRequiredError` (402) — insufficient credits
|
|
@@ -54,12 +76,16 @@ result = client.create_chat(
|
|
|
54
76
|
|
|
55
77
|
Continue an existing conversation. Same delivery modes as `create_chat()`.
|
|
56
78
|
|
|
79
|
+
⚠️ The default BLOCKS until the chat's entire next run completes. When relaying messages
|
|
80
|
+
to several chats, pass `delivery="send_only"` — otherwise the first send hangs your
|
|
81
|
+
script for that chat's whole run.
|
|
82
|
+
|
|
57
83
|
```python
|
|
58
84
|
result = client.send_message(
|
|
59
85
|
chat_id: str, # Existing chat (required)
|
|
60
86
|
message: str, # Follow-up message (required)
|
|
61
87
|
task_label: str = None, # Label for this message
|
|
62
|
-
delivery: str = "wait_for_completion",# "wait_for_completion"
|
|
88
|
+
delivery: str = "wait_for_completion",# "wait_for_completion" | "send_only" | "notify_on_completion"
|
|
63
89
|
timeout: int = 1800, # Max wait seconds (wait mode only)
|
|
64
90
|
notify_session_key: str = None, # OpenClaw session key (notify mode only)
|
|
65
91
|
gateway_url: str = None, # OpenClaw Gateway URL (notify mode only)
|
|
@@ -81,7 +107,7 @@ completion = client.wait_for_completion(
|
|
|
81
107
|
|
|
82
108
|
**Returns:** Same `{chat_id, is_operating, status, message}` shape.
|
|
83
109
|
- `status: "completed"` — chat finished, `message` contains full results
|
|
84
|
-
- `status: "
|
|
110
|
+
- `status: "operating"` + `timed_out: True` — still working server-side, `message` contains progress; resume with another `wait_for_completion()`
|
|
85
111
|
|
|
86
112
|
### get_history()
|
|
87
113
|
|
|
@@ -104,7 +130,50 @@ Quick status check (no message content).
|
|
|
104
130
|
status = client.get_status(chat_id: str)
|
|
105
131
|
```
|
|
106
132
|
|
|
107
|
-
**Returns:** `{"is_operating": bool, "status": str, "name": str, ...}`
|
|
133
|
+
**Returns:** `{"is_operating": bool, "status": str, "name": str, "latest_update": str | None, ...}`
|
|
134
|
+
|
|
135
|
+
`latest_update` carries the agent's most recent progress one-liner — poll it for live
|
|
136
|
+
orchestration dashboards. `None` until the agent posts its first update of a run.
|
|
137
|
+
|
|
138
|
+
### get_browser_status()
|
|
139
|
+
|
|
140
|
+
Browse availability on the user's machine — populate a profile picker from this.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
status = client.get_browser_status()
|
|
144
|
+
# {
|
|
145
|
+
# "extension_installed": bool,
|
|
146
|
+
# "chrome_running": bool,
|
|
147
|
+
# "active_profile": {...} | None,
|
|
148
|
+
# "available_profiles": [
|
|
149
|
+
# {"profileDir": "Default", "profileName": "Nitish", "hostedDomain": None,
|
|
150
|
+
# "hasExtension": True, "connected": True}, ...
|
|
151
|
+
# ],
|
|
152
|
+
# "agent_tab_count": int,
|
|
153
|
+
# }
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Pass a profile's `profileDir` as `browser_profile_id` in `create_chat(enable_browse=True)`.
|
|
157
|
+
|
|
158
|
+
### list_toolkits()
|
|
159
|
+
|
|
160
|
+
The SaaS toolkit catalog with the user's connection state — populate a tool picker from this.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
toolkits = client.list_toolkits(connected_only=True) # only what the user connected
|
|
164
|
+
# [{"slug": "gmail", "name": "Gmail", "connection": {...}, ...}, ...]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Pass toolkit `slug` values as `tools_selection` in `create_chat(enable_tools=True)`.
|
|
168
|
+
|
|
169
|
+
### list_toolkit_tools()
|
|
170
|
+
|
|
171
|
+
The individual tools a toolkit carries (for showing what enabling it unlocks).
|
|
172
|
+
`tools_selection` itself takes TOOLKIT slugs, not tool slugs.
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
tools = client.list_toolkit_tools("gmail")
|
|
176
|
+
```
|
|
108
177
|
|
|
109
178
|
### delete_chat()
|
|
110
179
|
|
|
@@ -14,6 +14,7 @@ import requests
|
|
|
14
14
|
|
|
15
15
|
from .config import Config
|
|
16
16
|
from .exceptions import FileDownloadError, FileUploadError
|
|
17
|
+
from .state_paths import state_path
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class FileProcessor:
|
|
@@ -36,7 +37,7 @@ class FileProcessor:
|
|
|
36
37
|
|
|
37
38
|
def __init__(self, config: Config):
|
|
38
39
|
self.config = config
|
|
39
|
-
self.default_download_dir =
|
|
40
|
+
self.default_download_dir = state_path("chats")
|
|
40
41
|
|
|
41
42
|
def transform_outgoing(self, message: str) -> tuple[str, list]:
|
|
42
43
|
"""
|
|
@@ -16,6 +16,7 @@ from .message_formatter import (
|
|
|
16
16
|
format_messages_for_delivery,
|
|
17
17
|
session_key_to_filename,
|
|
18
18
|
)
|
|
19
|
+
from .state_paths import state_path
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
21
22
|
from .config import Config
|
|
@@ -63,7 +64,7 @@ class MessageProcessor:
|
|
|
63
64
|
"""
|
|
64
65
|
self.config = config
|
|
65
66
|
self.files = file_processor
|
|
66
|
-
self.chats_dir =
|
|
67
|
+
self.chats_dir = state_path("chats")
|
|
67
68
|
|
|
68
69
|
def process_for_delivery(
|
|
69
70
|
self,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CellCog SDK state-directory resolution (CEL-1300 item 1).
|
|
3
|
+
|
|
4
|
+
All SDK state (tracked chats, downloaded files, daemon pid/heartbeat/version/log,
|
|
5
|
+
seen indices) lives under ONE base directory, resolved once per process:
|
|
6
|
+
|
|
7
|
+
1. $CELLCOG_STATE_DIR — explicit override, always wins.
|
|
8
|
+
2. ~/.cellcog — the default, when the home directory is usable and writable.
|
|
9
|
+
3. $TMPDIR/cellcog-state-{uid} — fallback for headless/container environments
|
|
10
|
+
where HOME is unset, '/', or not writable (e.g. Docker with HOME=/). A
|
|
11
|
+
one-line note is printed to stderr the first time the fallback engages,
|
|
12
|
+
naming CELLCOG_STATE_DIR as the override.
|
|
13
|
+
|
|
14
|
+
Historically the SDK hardcoded Path("~/.cellcog").expanduser() everywhere, so a
|
|
15
|
+
container with HOME=/ crashed the CellCogClient constructor with a raw
|
|
16
|
+
PermissionError before any API call. Every path helper now routes through
|
|
17
|
+
state_dir()/state_path() — never hardcode ~/.cellcog anywhere else.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
_resolved: Optional[Path] = None
|
|
27
|
+
_fallback_noted = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_dir_usable(path: Path) -> bool:
|
|
31
|
+
"""True if `path` exists (or can be created) and is writable."""
|
|
32
|
+
try:
|
|
33
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
probe = path / ".cellcog_write_probe"
|
|
35
|
+
probe.touch()
|
|
36
|
+
probe.unlink(missing_ok=True)
|
|
37
|
+
return True
|
|
38
|
+
except Exception:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def state_dir() -> Path:
|
|
43
|
+
"""Resolve (once per process) and return the SDK state base directory."""
|
|
44
|
+
global _resolved, _fallback_noted
|
|
45
|
+
if _resolved is not None:
|
|
46
|
+
return _resolved
|
|
47
|
+
|
|
48
|
+
override = os.environ.get("CELLCOG_STATE_DIR")
|
|
49
|
+
if override:
|
|
50
|
+
candidate = Path(override).expanduser()
|
|
51
|
+
if _is_dir_usable(candidate):
|
|
52
|
+
_resolved = candidate
|
|
53
|
+
return _resolved
|
|
54
|
+
print(
|
|
55
|
+
f"Warning: CELLCOG_STATE_DIR={override!r} is not writable; "
|
|
56
|
+
"falling back to default state-dir resolution.",
|
|
57
|
+
file=sys.stderr,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
home_candidate = Path("~/.cellcog").expanduser()
|
|
61
|
+
# Guard the degenerate expansion ('/.cellcog' when HOME is '/' or unset):
|
|
62
|
+
# even where root could write it, polluting the fs root is never the intent.
|
|
63
|
+
if str(home_candidate) != "/.cellcog" and _is_dir_usable(home_candidate):
|
|
64
|
+
_resolved = home_candidate
|
|
65
|
+
return _resolved
|
|
66
|
+
|
|
67
|
+
uid = os.getuid() if hasattr(os, "getuid") else "user"
|
|
68
|
+
fallback = Path(tempfile.gettempdir()) / f"cellcog-state-{uid}"
|
|
69
|
+
_is_dir_usable(fallback) # best-effort create
|
|
70
|
+
if not _fallback_noted:
|
|
71
|
+
print(
|
|
72
|
+
f"Note: using {fallback} for CellCog SDK state (home directory not "
|
|
73
|
+
"writable). Set CELLCOG_STATE_DIR to choose a persistent location.",
|
|
74
|
+
file=sys.stderr,
|
|
75
|
+
)
|
|
76
|
+
_fallback_noted = True
|
|
77
|
+
_resolved = fallback
|
|
78
|
+
return _resolved
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def state_path(*parts: str) -> Path:
|
|
82
|
+
"""A path under the state dir, e.g. state_path('chats') or state_path('daemon.pid')."""
|
|
83
|
+
return state_dir().joinpath(*parts)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cellcog"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.4.0"
|
|
8
8
|
description = "Python SDK for CellCog - Any-to-Any AI for agents. Fire-and-forget pattern with WebSocket notifications."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|