soothe-daemon 0.5.6__py3-none-any.whl
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.
- soothe_daemon/README.md +122 -0
- soothe_daemon/__init__.py +9 -0
- soothe_daemon/__main__.py +6 -0
- soothe_daemon/_handlers.py +288 -0
- soothe_daemon/_rpc_handlers.py +406 -0
- soothe_daemon/bootstrap_env.py +35 -0
- soothe_daemon/cli/__init__.py +1 -0
- soothe_daemon/cli/daemon_main.py +355 -0
- soothe_daemon/client_session.py +369 -0
- soothe_daemon/config/__init__.py +29 -0
- soothe_daemon/config/env.py +23 -0
- soothe_daemon/config/models.py +252 -0
- soothe_daemon/config/settings.py +167 -0
- soothe_daemon/entrypoint.py +121 -0
- soothe_daemon/event_bus.py +273 -0
- soothe_daemon/event_size_stats.py +166 -0
- soothe_daemon/health/__init__.py +50 -0
- soothe_daemon/health/checker.py +243 -0
- soothe_daemon/health/checks/__init__.py +3 -0
- soothe_daemon/health/checks/config_check.py +196 -0
- soothe_daemon/health/checks/daemon_check.py +673 -0
- soothe_daemon/health/checks/embedding_warmup_check.py +92 -0
- soothe_daemon/health/checks/external_apis_check.py +177 -0
- soothe_daemon/health/checks/mcp_check.py +118 -0
- soothe_daemon/health/checks/observability_check.py +162 -0
- soothe_daemon/health/checks/persistence_check.py +242 -0
- soothe_daemon/health/checks/protocols_check.py +66 -0
- soothe_daemon/health/checks/providers_check.py +143 -0
- soothe_daemon/health/checks/vector_stores_check.py +156 -0
- soothe_daemon/health/formatters.py +194 -0
- soothe_daemon/health/models.py +133 -0
- soothe_daemon/image_understanding.py +177 -0
- soothe_daemon/logging.py +90 -0
- soothe_daemon/loop_isolation.py +177 -0
- soothe_daemon/message_router.py +1486 -0
- soothe_daemon/paths.py +23 -0
- soothe_daemon/protocol_v2.py +154 -0
- soothe_daemon/query_engine.py +816 -0
- soothe_daemon/reattachment_handler.py +122 -0
- soothe_daemon/runner/__init__.py +10 -0
- soothe_daemon/runner/factory.py +93 -0
- soothe_daemon/runner/pool_runner.py +1500 -0
- soothe_daemon/runner/ray_actor.py +59 -0
- soothe_daemon/runner/ray_runner.py +98 -0
- soothe_daemon/server.py +1086 -0
- soothe_daemon/singleton.py +67 -0
- soothe_daemon/thread_state.py +125 -0
- soothe_daemon/transport_manager.py +214 -0
- soothe_daemon/transports/__init__.py +5 -0
- soothe_daemon/transports/base.py +141 -0
- soothe_daemon/transports/http_rest.py +490 -0
- soothe_daemon/transports/websocket.py +326 -0
- soothe_daemon-0.5.6.dist-info/METADATA +75 -0
- soothe_daemon-0.5.6.dist-info/RECORD +56 -0
- soothe_daemon-0.5.6.dist-info/WHEEL +4 -0
- soothe_daemon-0.5.6.dist-info/entry_points.txt +2 -0
soothe_daemon/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# soothe.daemon
|
|
2
|
+
|
|
3
|
+
Long-running background process that serves the Soothe agent over multiple
|
|
4
|
+
transports. Acts as a **transport adapter** around `SootheRunner` — it does
|
|
5
|
+
not re-implement orchestration logic.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Relationship to `soothe.core`
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌──────────────────────────────────────────┐
|
|
13
|
+
│ TUI / CLI client │
|
|
14
|
+
└───────────────┬──────────────────────────┘
|
|
15
|
+
│ WebSocket / HTTP REST
|
|
16
|
+
┌───────────────▼──────────────────────────┐
|
|
17
|
+
│ soothe.daemon │
|
|
18
|
+
│ │
|
|
19
|
+
│ SootheDaemon process lifecycle │
|
|
20
|
+
│ TransportManager multi-transport │
|
|
21
|
+
│ MessageRouter JSON → runner API │
|
|
22
|
+
│ QueryEngine streaming + cancel│
|
|
23
|
+
│ ThreadStateRegistry per-thread state │
|
|
24
|
+
└───────────────┬──────────────────────────┘
|
|
25
|
+
│ constructs / calls
|
|
26
|
+
┌───────────────▼──────────────────────────┐
|
|
27
|
+
│ soothe.core.runner.SootheRunner │
|
|
28
|
+
│ (orchestration, protocols, streaming) │
|
|
29
|
+
└──────────────────────────────────────────┘
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`SootheDaemon` holds a single `SootheRunner` instance and delegates all
|
|
33
|
+
query execution to it via public APIs (`astream`, thread helpers). The
|
|
34
|
+
daemon **never** duplicates protocol, memory, or planning logic.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Directory map
|
|
39
|
+
|
|
40
|
+
| File / Package | Responsibility |
|
|
41
|
+
|----------------|----------------|
|
|
42
|
+
| `server.py` | `SootheDaemon` — process lifecycle, WebSocket server, Unix socket |
|
|
43
|
+
| `entrypoint.py` | `run_daemon()` — CLI entry point, signal handling |
|
|
44
|
+
| `transport_manager.py` | Manages multiple transport servers (WebSocket, HTTP REST) |
|
|
45
|
+
| `transports/` | `WebSocketTransport`, `HttpRestTransport`, `TransportServer` base |
|
|
46
|
+
| `message_router.py` | Routes incoming JSON messages to runner public APIs |
|
|
47
|
+
| `query_engine.py` | `QueryEngine` — streams a single query, owns cancel / ownership |
|
|
48
|
+
| `thread_state.py` | `ThreadStateRegistry` — per-thread draft, history, logger |
|
|
49
|
+
| `client_session.py` | Tracks connected client metadata and event filtering |
|
|
50
|
+
| `event_bus.py` | In-process pub/sub for broadcasting events to all clients |
|
|
51
|
+
| `protocol.py` / `protocol_v2.py` | Wire-format encode/decode helpers |
|
|
52
|
+
| `websocket_client.py` | `WebSocketClient` — for CLI commands that talk to the daemon |
|
|
53
|
+
| `singleton.py` | Single-instance enforcement |
|
|
54
|
+
| `paths.py` | `pid_path()`, `socket_path()` — canonical filesystem paths |
|
|
55
|
+
| `health/` | `HealthChecker` and per-category check implementations |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## health/ subpackage
|
|
60
|
+
|
|
61
|
+
Health checks verify all Soothe components including daemon socket,
|
|
62
|
+
persistence, providers, protocols, and external APIs.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
daemon/health/
|
|
66
|
+
├── __init__.py # HealthChecker, format_* exports
|
|
67
|
+
├── checker.py # HealthChecker orchestrator
|
|
68
|
+
├── models.py # CheckResult, CategoryResult, HealthReport
|
|
69
|
+
├── formatters.py # format_text, format_markdown, format_json
|
|
70
|
+
└── checks/
|
|
71
|
+
├── config_check.py
|
|
72
|
+
├── daemon_check.py # uses soothe_daemon.paths (pid_path, socket_path)
|
|
73
|
+
├── persistence_check.py
|
|
74
|
+
├── protocols_check.py
|
|
75
|
+
├── providers_check.py
|
|
76
|
+
├── vector_stores_check.py
|
|
77
|
+
├── mcp_check.py
|
|
78
|
+
├── external_apis_check.py
|
|
79
|
+
└── observability_check.py
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Health checks live here (not in `core`) because they legitimately depend
|
|
83
|
+
on daemon-layer paths (`pid_path`, `socket_path`) and daemon connectivity.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Boundary rules
|
|
88
|
+
|
|
89
|
+
| Direction | Rule |
|
|
90
|
+
|-----------|------|
|
|
91
|
+
| `daemon` → `core` | OK — daemon composes `SootheRunner` |
|
|
92
|
+
| `daemon` → `soothe.logging` | OK |
|
|
93
|
+
| `daemon` → `config` | OK |
|
|
94
|
+
| `daemon` → `ux` | **Forbidden** |
|
|
95
|
+
| `daemon.health` → `daemon.paths` | OK — intra-daemon import |
|
|
96
|
+
| Orchestration logic in daemon | **Forbidden** — belongs in `core` |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Key types
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from soothe_daemon import SootheDaemon # main daemon class
|
|
104
|
+
from soothe_daemon import WebSocketClient # client for CLI ↔ daemon
|
|
105
|
+
from soothe_daemon import run_daemon # entrypoint
|
|
106
|
+
from soothe_daemon import pid_path # ~/.soothe/soothe.pid
|
|
107
|
+
from soothe_daemon import socket_path # ~/.soothe/soothe.sock
|
|
108
|
+
from soothe_daemon.health import HealthChecker
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Message flow
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
Client connects (WebSocket / HTTP)
|
|
117
|
+
→ TransportManager routes connection to handler
|
|
118
|
+
→ MessageRouter.handle(msg) dispatches by msg["type"]
|
|
119
|
+
→ QueryEngine.stream(runner, query, thread_id, ...)
|
|
120
|
+
→ runner.astream(...) yields (namespace, mode, data)
|
|
121
|
+
→ events broadcast via EventBus to all clients
|
|
122
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Soothe daemon subpackage - background agent runner with WebSocket IPC."""
|
|
2
|
+
|
|
3
|
+
from soothe_sdk.client import WebSocketClient
|
|
4
|
+
|
|
5
|
+
from soothe_daemon.entrypoint import run_daemon
|
|
6
|
+
from soothe_daemon.paths import pid_path, socket_path
|
|
7
|
+
from soothe_daemon.server import SootheDaemon
|
|
8
|
+
|
|
9
|
+
__all__ = ["SootheDaemon", "WebSocketClient", "pid_path", "run_daemon", "socket_path"]
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Client connection handling for the daemon (IG-110).
|
|
2
|
+
|
|
3
|
+
Heavy logic lives in ``message_router`` and ``query_engine``; this mixin wires
|
|
4
|
+
transport entrypoints and the input queue loop.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import contextlib
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import websockets.exceptions
|
|
15
|
+
from soothe.core.events import ERROR
|
|
16
|
+
from soothe_sdk.client.protocol import decode, encode
|
|
17
|
+
|
|
18
|
+
# Import RPC command handlers (RFC-404)
|
|
19
|
+
from soothe_daemon._rpc_handlers import (
|
|
20
|
+
_cmd_autopilot_dashboard,
|
|
21
|
+
_cmd_cancel,
|
|
22
|
+
_cmd_clear,
|
|
23
|
+
_cmd_config,
|
|
24
|
+
_cmd_detach,
|
|
25
|
+
_cmd_exit,
|
|
26
|
+
_cmd_history,
|
|
27
|
+
_cmd_memory,
|
|
28
|
+
_cmd_plan,
|
|
29
|
+
_cmd_policy,
|
|
30
|
+
_cmd_quit,
|
|
31
|
+
_cmd_resume,
|
|
32
|
+
_cmd_review,
|
|
33
|
+
_cmd_thread,
|
|
34
|
+
_handle_command_request,
|
|
35
|
+
_send_command_response,
|
|
36
|
+
)
|
|
37
|
+
from soothe_daemon.message_router import (
|
|
38
|
+
_coerce_loop_input_text,
|
|
39
|
+
_queue_options_from_daemon_message,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DaemonHandlersMixin:
|
|
46
|
+
"""Client connection handling and query execution mixin.
|
|
47
|
+
|
|
48
|
+
Mixed into ``SootheDaemon`` -- all ``self.*`` attributes are defined
|
|
49
|
+
on the concrete class.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# Attach RPC handlers to mixin (RFC-404)
|
|
53
|
+
_handle_command_request = _handle_command_request
|
|
54
|
+
_send_command_response = _send_command_response
|
|
55
|
+
_cmd_clear = _cmd_clear
|
|
56
|
+
_cmd_exit = _cmd_exit
|
|
57
|
+
_cmd_quit = _cmd_quit
|
|
58
|
+
_cmd_detach = _cmd_detach
|
|
59
|
+
_cmd_cancel = _cmd_cancel
|
|
60
|
+
_cmd_memory = _cmd_memory
|
|
61
|
+
_cmd_policy = _cmd_policy
|
|
62
|
+
_cmd_history = _cmd_history
|
|
63
|
+
_cmd_config = _cmd_config
|
|
64
|
+
_cmd_review = _cmd_review
|
|
65
|
+
_cmd_plan = _cmd_plan
|
|
66
|
+
_cmd_thread = _cmd_thread
|
|
67
|
+
_cmd_resume = _cmd_resume
|
|
68
|
+
_cmd_autopilot_dashboard = _cmd_autopilot_dashboard
|
|
69
|
+
|
|
70
|
+
async def _send_client_message(self, client_id: Any, msg: dict[str, Any]) -> None:
|
|
71
|
+
"""Send a direct response to a specific client when possible.
|
|
72
|
+
|
|
73
|
+
Handles normal client disconnects gracefully without logging errors.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
session = (
|
|
77
|
+
await self._session_manager.get_session(client_id)
|
|
78
|
+
if isinstance(client_id, str)
|
|
79
|
+
else None
|
|
80
|
+
)
|
|
81
|
+
if session is not None:
|
|
82
|
+
await session.transport.send(session.transport_client, msg)
|
|
83
|
+
return
|
|
84
|
+
if hasattr(client_id, "writer"):
|
|
85
|
+
await self._send(client_id, msg)
|
|
86
|
+
except websockets.exceptions.ConnectionClosedOK:
|
|
87
|
+
# Normal disconnect (code 1000) - expected, no error logging
|
|
88
|
+
logger.debug("Client %r disconnected normally", client_id)
|
|
89
|
+
except (websockets.exceptions.ConnectionClosedError, ConnectionError):
|
|
90
|
+
# Abnormal disconnect - log as warning without full traceback
|
|
91
|
+
logger.debug("Client %r disconnected unexpectedly", client_id)
|
|
92
|
+
except Exception:
|
|
93
|
+
# Unexpected error - log with full traceback
|
|
94
|
+
logger.debug("Failed to send direct response to client %r", client_id, exc_info=True)
|
|
95
|
+
|
|
96
|
+
async def _handle_client(
|
|
97
|
+
self,
|
|
98
|
+
reader: asyncio.StreamReader,
|
|
99
|
+
writer: asyncio.StreamWriter,
|
|
100
|
+
) -> None:
|
|
101
|
+
from soothe_daemon.server import _ClientConn
|
|
102
|
+
|
|
103
|
+
client = _ClientConn(reader=reader, writer=writer)
|
|
104
|
+
self._clients.append(client)
|
|
105
|
+
logger.info("Client connected (total=%d)", len(self._clients))
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
initial_state = (
|
|
109
|
+
"running" if self._query_running else ("idle" if self._running else "stopped")
|
|
110
|
+
)
|
|
111
|
+
initial_msg = {
|
|
112
|
+
"type": "status",
|
|
113
|
+
"state": initial_state,
|
|
114
|
+
"input_history": [],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
client.writer.write(encode(initial_msg))
|
|
118
|
+
client.writer.write(encode(self.daemon_ready_message()))
|
|
119
|
+
await client.writer.drain()
|
|
120
|
+
except Exception:
|
|
121
|
+
logger.exception("Failed to send initial status to client")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
while True:
|
|
125
|
+
line = await reader.readline()
|
|
126
|
+
if not line:
|
|
127
|
+
break
|
|
128
|
+
msg = decode(line)
|
|
129
|
+
if msg is None:
|
|
130
|
+
continue
|
|
131
|
+
await self._message_router.dispatch(f"legacy:{id(client)}", msg)
|
|
132
|
+
except (asyncio.CancelledError, ConnectionError):
|
|
133
|
+
pass
|
|
134
|
+
finally:
|
|
135
|
+
self._clients = [c for c in self._clients if c is not client]
|
|
136
|
+
with contextlib.suppress(Exception):
|
|
137
|
+
writer.close()
|
|
138
|
+
await writer.wait_closed()
|
|
139
|
+
logger.info("Client disconnected (total=%d)", len(self._clients))
|
|
140
|
+
|
|
141
|
+
async def _handle_client_message(self, client_id: str, msg: dict[str, Any]) -> None:
|
|
142
|
+
"""Handle a message from a client (WebSocket / HTTP transports)."""
|
|
143
|
+
await self._message_router.dispatch(client_id, msg)
|
|
144
|
+
|
|
145
|
+
async def _process_loop_input_message(self, loop_id: str, msg: dict[str, Any]) -> None:
|
|
146
|
+
"""Process one loop-scoped message from ``LoopInputDispatcher`` (IG-408).
|
|
147
|
+
|
|
148
|
+
Supported ``msg["type"]`` values for user turns: ``input`` (normalized queue
|
|
149
|
+
payload from ``loop_input`` RPC) or ``loop_input`` (wire-shaped dict with
|
|
150
|
+
``content``). Other types are ignored with a warning except ``command`` and
|
|
151
|
+
``command_request``, which are handled above.
|
|
152
|
+
"""
|
|
153
|
+
from soothe_daemon.loop_isolation import bind_execution_thread_for_loop
|
|
154
|
+
|
|
155
|
+
msg_type = msg.get("type", "")
|
|
156
|
+
try:
|
|
157
|
+
checkpoint_thread_id = await bind_execution_thread_for_loop(self, loop_id)
|
|
158
|
+
except Exception as exc:
|
|
159
|
+
logger.warning(
|
|
160
|
+
"Failed to bind LangGraph checkpoint for loop %s: %s",
|
|
161
|
+
loop_id,
|
|
162
|
+
exc,
|
|
163
|
+
)
|
|
164
|
+
client_id = msg.get("client_id")
|
|
165
|
+
if client_id:
|
|
166
|
+
await self._send_client_message(
|
|
167
|
+
client_id,
|
|
168
|
+
{"type": "error", "code": "LOOP_CONTEXT", "message": str(exc)},
|
|
169
|
+
)
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
if msg_type == "command":
|
|
174
|
+
cmd = msg.get("cmd", "")
|
|
175
|
+
if cmd in ("/exit", "/quit"):
|
|
176
|
+
logger.warning(
|
|
177
|
+
"Received %s in loop worker — should be handled in MessageRouter",
|
|
178
|
+
cmd,
|
|
179
|
+
)
|
|
180
|
+
return
|
|
181
|
+
if cmd.strip().lower() == "/cancel":
|
|
182
|
+
if self._query_engine is not None:
|
|
183
|
+
await self._query_engine.cancel_loop(loop_id)
|
|
184
|
+
return
|
|
185
|
+
logger.warning("Received legacy 'command' message in loop worker — ignoring")
|
|
186
|
+
return
|
|
187
|
+
if msg_type == "command_request":
|
|
188
|
+
req = dict(msg)
|
|
189
|
+
req.setdefault("loop_id", loop_id)
|
|
190
|
+
await self._handle_command_request(req)
|
|
191
|
+
return
|
|
192
|
+
if msg_type not in ("input", "loop_input"):
|
|
193
|
+
logger.warning(
|
|
194
|
+
"Loop worker ignoring unsupported queue message type=%r loop_id=%s",
|
|
195
|
+
msg_type,
|
|
196
|
+
loop_id[:16] if loop_id else "?",
|
|
197
|
+
)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if msg_type == "loop_input":
|
|
201
|
+
prompt_text = _coerce_loop_input_text(msg.get("content"))
|
|
202
|
+
if prompt_text is None:
|
|
203
|
+
logger.warning(
|
|
204
|
+
"Loop worker loop_input missing usable content loop_id=%s",
|
|
205
|
+
loop_id[:16] if loop_id else "?",
|
|
206
|
+
)
|
|
207
|
+
return
|
|
208
|
+
else:
|
|
209
|
+
raw_text = msg.get("text")
|
|
210
|
+
if not isinstance(raw_text, str):
|
|
211
|
+
logger.warning(
|
|
212
|
+
"Loop worker input missing str text loop_id=%s",
|
|
213
|
+
loop_id[:16] if loop_id else "?",
|
|
214
|
+
)
|
|
215
|
+
return
|
|
216
|
+
prompt_text = raw_text
|
|
217
|
+
|
|
218
|
+
if self._query_engine is not None:
|
|
219
|
+
qo = _queue_options_from_daemon_message(msg)
|
|
220
|
+
model_params = qo["model_params"]
|
|
221
|
+
model_kw = qo["model"]
|
|
222
|
+
intent_hint = qo["intent_hint"]
|
|
223
|
+
raw_att = msg.get("attachments")
|
|
224
|
+
attachments = raw_att if isinstance(raw_att, list) and raw_att else None
|
|
225
|
+
await self._query_engine.run_query(
|
|
226
|
+
prompt_text,
|
|
227
|
+
loop_id=loop_id,
|
|
228
|
+
autonomous=qo["autonomous"],
|
|
229
|
+
max_iterations=qo["max_iterations"],
|
|
230
|
+
preferred_subagent=qo["preferred_subagent"],
|
|
231
|
+
client_id=msg.get("client_id"),
|
|
232
|
+
interactive=qo["interactive"],
|
|
233
|
+
model=model_kw,
|
|
234
|
+
model_params=model_params,
|
|
235
|
+
attachments=attachments,
|
|
236
|
+
checkpoint_thread_id=checkpoint_thread_id,
|
|
237
|
+
intent_hint=intent_hint,
|
|
238
|
+
)
|
|
239
|
+
except Exception:
|
|
240
|
+
logger.exception("Daemon loop input handler error")
|
|
241
|
+
self._query_running = False
|
|
242
|
+
lid = str(loop_id or "").strip()
|
|
243
|
+
if lid and self._query_engine is not None:
|
|
244
|
+
qe = self._query_engine
|
|
245
|
+
await self._broadcast(
|
|
246
|
+
qe._loop_scoped_client_message(
|
|
247
|
+
lid,
|
|
248
|
+
{
|
|
249
|
+
"type": "event",
|
|
250
|
+
"namespace": [],
|
|
251
|
+
"mode": "custom",
|
|
252
|
+
"data": {"type": ERROR, "error": "Daemon failed to process input"},
|
|
253
|
+
},
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
await self._broadcast(
|
|
257
|
+
qe._loop_scoped_client_message(lid, {"type": "status", "state": "idle"})
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
async def _run_query(
|
|
261
|
+
self,
|
|
262
|
+
text: str,
|
|
263
|
+
*,
|
|
264
|
+
loop_id: str | None = None,
|
|
265
|
+
autonomous: bool = False,
|
|
266
|
+
max_iterations: int | None = None,
|
|
267
|
+
preferred_subagent: str | None = None,
|
|
268
|
+
client_id: str | None = None,
|
|
269
|
+
interactive: bool = False,
|
|
270
|
+
model: str | None = None,
|
|
271
|
+
model_params: dict | None = None,
|
|
272
|
+
attachments: list[dict[str, str]] | None = None,
|
|
273
|
+
checkpoint_thread_id: str | None = None,
|
|
274
|
+
) -> None:
|
|
275
|
+
"""Delegate to ``QueryEngine`` (keeps unit tests and legacy callers working)."""
|
|
276
|
+
await self._query_engine.run_query(
|
|
277
|
+
text,
|
|
278
|
+
loop_id=loop_id,
|
|
279
|
+
autonomous=autonomous,
|
|
280
|
+
max_iterations=max_iterations,
|
|
281
|
+
preferred_subagent=preferred_subagent,
|
|
282
|
+
client_id=client_id,
|
|
283
|
+
interactive=interactive,
|
|
284
|
+
model=model,
|
|
285
|
+
model_params=model_params,
|
|
286
|
+
attachments=attachments,
|
|
287
|
+
checkpoint_thread_id=checkpoint_thread_id,
|
|
288
|
+
)
|