vibe-remote 2.1.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.
- config/__init__.py +37 -0
- config/paths.py +56 -0
- config/v2_compat.py +74 -0
- config/v2_config.py +206 -0
- config/v2_sessions.py +73 -0
- config/v2_settings.py +115 -0
- core/__init__.py +0 -0
- core/controller.py +736 -0
- core/handlers/__init__.py +13 -0
- core/handlers/command_handlers.py +342 -0
- core/handlers/message_handler.py +365 -0
- core/handlers/session_handler.py +233 -0
- core/handlers/settings_handler.py +362 -0
- modules/__init__.py +0 -0
- modules/agent_router.py +58 -0
- modules/agents/__init__.py +38 -0
- modules/agents/base.py +91 -0
- modules/agents/claude_agent.py +344 -0
- modules/agents/codex_agent.py +368 -0
- modules/agents/opencode_agent.py +2155 -0
- modules/agents/service.py +41 -0
- modules/agents/subagent_router.py +136 -0
- modules/claude_client.py +154 -0
- modules/im/__init__.py +63 -0
- modules/im/base.py +323 -0
- modules/im/factory.py +60 -0
- modules/im/formatters/__init__.py +4 -0
- modules/im/formatters/base_formatter.py +639 -0
- modules/im/formatters/slack_formatter.py +127 -0
- modules/im/slack.py +2091 -0
- modules/session_manager.py +138 -0
- modules/settings_manager.py +587 -0
- vibe/__init__.py +6 -0
- vibe/__main__.py +12 -0
- vibe/_version.py +34 -0
- vibe/api.py +412 -0
- vibe/cli.py +637 -0
- vibe/runtime.py +213 -0
- vibe/service_main.py +101 -0
- vibe/templates/slack_manifest.json +65 -0
- vibe/ui/dist/assets/index-8g3mNwMK.js +35 -0
- vibe/ui/dist/assets/index-M55aMB5R.css +1 -0
- vibe/ui/dist/assets/logo-BzryTZ7u.png +0 -0
- vibe/ui/dist/index.html +17 -0
- vibe/ui/dist/logo.png +0 -0
- vibe/ui/dist/vite.svg +1 -0
- vibe/ui_server.py +346 -0
- vibe_remote-2.1.6.dist-info/METADATA +295 -0
- vibe_remote-2.1.6.dist-info/RECORD +52 -0
- vibe_remote-2.1.6.dist-info/WHEEL +4 -0
- vibe_remote-2.1.6.dist-info/entry_points.txt +2 -0
- vibe_remote-2.1.6.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
from asyncio.subprocess import Process
|
|
7
|
+
from typing import Dict, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from markdown_to_mrkdwn import SlackMarkdownConverter
|
|
10
|
+
|
|
11
|
+
from modules.agents.base import AgentRequest, BaseAgent
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
STREAM_BUFFER_LIMIT = 8 * 1024 * 1024 # 8MB cap for Codex stdout/stderr streams
|
|
16
|
+
|
|
17
|
+
class CodexAgent(BaseAgent):
|
|
18
|
+
"""Codex CLI integration via codex exec JSON streaming mode."""
|
|
19
|
+
|
|
20
|
+
name = "codex"
|
|
21
|
+
|
|
22
|
+
def __init__(self, controller, codex_config):
|
|
23
|
+
super().__init__(controller)
|
|
24
|
+
self.codex_config = codex_config
|
|
25
|
+
self.active_processes: Dict[str, Tuple[Process, str]] = {}
|
|
26
|
+
self.base_process_index: Dict[str, str] = {}
|
|
27
|
+
self.composite_to_base: Dict[str, str] = {}
|
|
28
|
+
self._initialized_sessions: set[str] = set()
|
|
29
|
+
self._pending_assistant_messages: Dict[str, Tuple[str, Optional[str]]] = {}
|
|
30
|
+
self._slack_markdown_converter = (
|
|
31
|
+
SlackMarkdownConverter()
|
|
32
|
+
if getattr(self.controller.config, "platform", None) == "slack"
|
|
33
|
+
else None
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def handle_message(self, request: AgentRequest) -> None:
|
|
37
|
+
existing = self.base_process_index.get(request.base_session_id)
|
|
38
|
+
if existing and existing in self.active_processes:
|
|
39
|
+
await self.controller.emit_agent_message(
|
|
40
|
+
request.context,
|
|
41
|
+
"notify",
|
|
42
|
+
"⚠️ Codex is already processing a task in this thread. "
|
|
43
|
+
"Cancelling the previous run...",
|
|
44
|
+
)
|
|
45
|
+
await self._terminate_process(existing)
|
|
46
|
+
await self.controller.emit_agent_message(
|
|
47
|
+
request.context,
|
|
48
|
+
"notify",
|
|
49
|
+
"⏹ Previous Codex task cancelled. Starting the new request...",
|
|
50
|
+
)
|
|
51
|
+
resume_id = self.settings_manager.get_agent_session_id(
|
|
52
|
+
request.settings_key,
|
|
53
|
+
request.base_session_id,
|
|
54
|
+
agent_name=self.name,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if not os.path.exists(request.working_path):
|
|
58
|
+
os.makedirs(request.working_path, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
cmd = self._build_command(request, resume_id)
|
|
61
|
+
try:
|
|
62
|
+
process = await asyncio.create_subprocess_exec(
|
|
63
|
+
*cmd,
|
|
64
|
+
stdout=asyncio.subprocess.PIPE,
|
|
65
|
+
stderr=asyncio.subprocess.PIPE,
|
|
66
|
+
cwd=request.working_path,
|
|
67
|
+
limit=STREAM_BUFFER_LIMIT,
|
|
68
|
+
**({"preexec_fn": os.setsid} if hasattr(os, "setsid") else {}),
|
|
69
|
+
)
|
|
70
|
+
except FileNotFoundError:
|
|
71
|
+
await self.controller.emit_agent_message(
|
|
72
|
+
request.context,
|
|
73
|
+
"notify",
|
|
74
|
+
"❌ Codex CLI not found. Please install it or set CODEX_CLI_PATH.",
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"Failed to launch Codex CLI: {e}", exc_info=True)
|
|
79
|
+
await self.controller.emit_agent_message(
|
|
80
|
+
request.context, "notify", f"❌ Failed to start Codex CLI: {e}"
|
|
81
|
+
)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
await self._delete_ack(request)
|
|
85
|
+
|
|
86
|
+
self.active_processes[request.composite_session_id] = (
|
|
87
|
+
process,
|
|
88
|
+
request.settings_key,
|
|
89
|
+
)
|
|
90
|
+
self.base_process_index[request.base_session_id] = request.composite_session_id
|
|
91
|
+
self.composite_to_base[request.composite_session_id] = request.base_session_id
|
|
92
|
+
logger.info(
|
|
93
|
+
f"Codex session {request.composite_session_id} started (pid={process.pid})"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
stdout_task = asyncio.create_task(
|
|
97
|
+
self._consume_stdout(process, request)
|
|
98
|
+
)
|
|
99
|
+
stderr_task = asyncio.create_task(
|
|
100
|
+
self._consume_stderr(process, request)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
await process.wait()
|
|
105
|
+
await asyncio.gather(stdout_task, stderr_task)
|
|
106
|
+
finally:
|
|
107
|
+
self._unregister_process(request.composite_session_id)
|
|
108
|
+
|
|
109
|
+
if process.returncode != 0:
|
|
110
|
+
await self.controller.emit_agent_message(
|
|
111
|
+
request.context,
|
|
112
|
+
"notify",
|
|
113
|
+
"⚠️ Codex exited with a non-zero status. Review stderr for details.",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
async def clear_sessions(self, settings_key: str) -> int:
|
|
117
|
+
self.settings_manager.clear_agent_sessions(settings_key, self.name)
|
|
118
|
+
# Terminate any active processes scoped to this settings key
|
|
119
|
+
terminated = 0
|
|
120
|
+
for key, (_, stored_key) in list(self.active_processes.items()):
|
|
121
|
+
if stored_key == settings_key:
|
|
122
|
+
await self._terminate_process(key)
|
|
123
|
+
terminated += 1
|
|
124
|
+
return terminated
|
|
125
|
+
|
|
126
|
+
async def handle_stop(self, request: AgentRequest) -> bool:
|
|
127
|
+
key = request.composite_session_id
|
|
128
|
+
if not await self._terminate_process(key):
|
|
129
|
+
key = self.base_process_index.get(request.base_session_id)
|
|
130
|
+
if not key or not await self._terminate_process(key):
|
|
131
|
+
return False
|
|
132
|
+
await self.controller.emit_agent_message(
|
|
133
|
+
request.context, "notify", "🛑 Terminated Codex execution."
|
|
134
|
+
)
|
|
135
|
+
logger.info(f"Codex session {key} terminated via /stop")
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
def _unregister_process(self, composite_key: str):
|
|
139
|
+
self.active_processes.pop(composite_key, None)
|
|
140
|
+
self._pending_assistant_messages.pop(composite_key, None)
|
|
141
|
+
base_id = self.composite_to_base.pop(composite_key, None)
|
|
142
|
+
if base_id and self.base_process_index.get(base_id) == composite_key:
|
|
143
|
+
self.base_process_index.pop(base_id, None)
|
|
144
|
+
|
|
145
|
+
async def _terminate_process(self, composite_key: str) -> bool:
|
|
146
|
+
entry = self.active_processes.get(composite_key)
|
|
147
|
+
if not entry:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
proc, _ = entry
|
|
151
|
+
try:
|
|
152
|
+
if hasattr(os, "getpgid"):
|
|
153
|
+
try:
|
|
154
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
155
|
+
except ProcessLookupError:
|
|
156
|
+
pass
|
|
157
|
+
else:
|
|
158
|
+
proc.kill()
|
|
159
|
+
await proc.wait()
|
|
160
|
+
except ProcessLookupError:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
self._unregister_process(composite_key)
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
def _build_command(self, request: AgentRequest, resume_id: Optional[str]) -> list:
|
|
167
|
+
cmd = [self.codex_config.binary, "exec", "--json"]
|
|
168
|
+
cmd += ["--dangerously-bypass-approvals-and-sandbox"]
|
|
169
|
+
cmd += ["--skip-git-repo-check"]
|
|
170
|
+
|
|
171
|
+
if self.codex_config.default_model:
|
|
172
|
+
cmd += ["--model", self.codex_config.default_model]
|
|
173
|
+
|
|
174
|
+
cmd += ["--cd", request.working_path]
|
|
175
|
+
cmd += self.codex_config.extra_args
|
|
176
|
+
|
|
177
|
+
if resume_id:
|
|
178
|
+
cmd += ["resume", resume_id]
|
|
179
|
+
|
|
180
|
+
cmd.append(request.message)
|
|
181
|
+
|
|
182
|
+
logger.info(f"Executing Codex command: {' '.join(cmd[:-1])} <prompt>")
|
|
183
|
+
return cmd
|
|
184
|
+
|
|
185
|
+
async def _consume_stdout(self, process: Process, request: AgentRequest):
|
|
186
|
+
assert process.stdout is not None
|
|
187
|
+
try:
|
|
188
|
+
while True:
|
|
189
|
+
try:
|
|
190
|
+
line = await process.stdout.readline()
|
|
191
|
+
except (asyncio.LimitOverrunError, ValueError) as err:
|
|
192
|
+
await self._notify_stream_error(
|
|
193
|
+
request, f"Codex output too long; stream decode failed: {err}"
|
|
194
|
+
)
|
|
195
|
+
logger.exception("Codex stdout exceeded buffer limit")
|
|
196
|
+
break
|
|
197
|
+
if not line:
|
|
198
|
+
break
|
|
199
|
+
line = line.decode().strip()
|
|
200
|
+
if not line:
|
|
201
|
+
continue
|
|
202
|
+
try:
|
|
203
|
+
event = json.loads(line)
|
|
204
|
+
except json.JSONDecodeError:
|
|
205
|
+
logger.debug(f"Codex emitted non-JSON line: {line}")
|
|
206
|
+
continue
|
|
207
|
+
await self._handle_event(event, request)
|
|
208
|
+
except Exception as err:
|
|
209
|
+
await self._notify_stream_error(
|
|
210
|
+
request, f"Codex stdout 读取异常:{err}"
|
|
211
|
+
)
|
|
212
|
+
logger.exception("Unexpected Codex stdout error")
|
|
213
|
+
|
|
214
|
+
async def _consume_stderr(self, process: Process, request: AgentRequest):
|
|
215
|
+
assert process.stderr is not None
|
|
216
|
+
buffer = []
|
|
217
|
+
while True:
|
|
218
|
+
line = await process.stderr.readline()
|
|
219
|
+
if not line:
|
|
220
|
+
break
|
|
221
|
+
decoded = line.decode(errors="ignore").rstrip()
|
|
222
|
+
buffer.append(decoded)
|
|
223
|
+
logger.debug(f"Codex stderr: {decoded}")
|
|
224
|
+
|
|
225
|
+
if buffer:
|
|
226
|
+
joined = "\n".join(buffer[-10:])
|
|
227
|
+
await self.controller.emit_agent_message(
|
|
228
|
+
request.context,
|
|
229
|
+
"notify",
|
|
230
|
+
f"❗️ Codex stderr:\n```stderr\n{joined}\n```",
|
|
231
|
+
parse_mode="markdown",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
async def _handle_event(
|
|
235
|
+
self, event: Dict, request: AgentRequest
|
|
236
|
+
):
|
|
237
|
+
event_type = event.get("type")
|
|
238
|
+
|
|
239
|
+
if event_type == "thread.started":
|
|
240
|
+
thread_id = event.get("thread_id")
|
|
241
|
+
if thread_id:
|
|
242
|
+
self.settings_manager.set_agent_session_mapping(
|
|
243
|
+
request.settings_key,
|
|
244
|
+
self.name,
|
|
245
|
+
request.base_session_id,
|
|
246
|
+
thread_id,
|
|
247
|
+
)
|
|
248
|
+
session_key = request.composite_session_id
|
|
249
|
+
if session_key not in self._initialized_sessions:
|
|
250
|
+
self._initialized_sessions.add(session_key)
|
|
251
|
+
system_text = self.im_client.formatter.format_system_message(
|
|
252
|
+
request.working_path, "init", thread_id
|
|
253
|
+
)
|
|
254
|
+
await self.controller.emit_agent_message(
|
|
255
|
+
request.context,
|
|
256
|
+
"system",
|
|
257
|
+
system_text,
|
|
258
|
+
parse_mode="markdown",
|
|
259
|
+
)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
if event_type == "item.completed":
|
|
263
|
+
details = event.get("item", {})
|
|
264
|
+
item_type = details.get("type")
|
|
265
|
+
|
|
266
|
+
if item_type == "agent_message":
|
|
267
|
+
text = details.get("text", "")
|
|
268
|
+
if text:
|
|
269
|
+
session_key = request.composite_session_id
|
|
270
|
+
pending = self._pending_assistant_messages.get(session_key)
|
|
271
|
+
if pending:
|
|
272
|
+
pending_text, pending_parse_mode = pending
|
|
273
|
+
await self.controller.emit_agent_message(
|
|
274
|
+
request.context,
|
|
275
|
+
"assistant",
|
|
276
|
+
pending_text,
|
|
277
|
+
parse_mode=pending_parse_mode or "markdown",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
self._pending_assistant_messages[session_key] = self._prepare_last_message_payload(text)
|
|
281
|
+
elif item_type == "command_execution":
|
|
282
|
+
command = details.get("command")
|
|
283
|
+
status = details.get("status")
|
|
284
|
+
if command:
|
|
285
|
+
toolcall = self.im_client.formatter.format_toolcall(
|
|
286
|
+
"bash",
|
|
287
|
+
{"command": command, "status": status},
|
|
288
|
+
)
|
|
289
|
+
await self.controller.emit_agent_message(
|
|
290
|
+
request.context,
|
|
291
|
+
"toolcall",
|
|
292
|
+
toolcall,
|
|
293
|
+
parse_mode="markdown",
|
|
294
|
+
)
|
|
295
|
+
elif item_type == "reasoning":
|
|
296
|
+
text = details.get("text", "")
|
|
297
|
+
if text:
|
|
298
|
+
await self.controller.emit_agent_message(
|
|
299
|
+
request.context,
|
|
300
|
+
"assistant",
|
|
301
|
+
f"_🧠 {text}_",
|
|
302
|
+
parse_mode="markdown",
|
|
303
|
+
)
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
if event_type == "error":
|
|
307
|
+
message = event.get("message", "Unknown error")
|
|
308
|
+
await self.controller.emit_agent_message(
|
|
309
|
+
request.context, "notify", f"❌ Codex error: {message}"
|
|
310
|
+
)
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
if event_type == "turn.failed":
|
|
314
|
+
error = event.get("error", {}).get("message", "Turn failed.")
|
|
315
|
+
await self.controller.emit_agent_message(
|
|
316
|
+
request.context, "notify", f"⚠️ Codex turn failed: {error}"
|
|
317
|
+
)
|
|
318
|
+
self._pending_assistant_messages.pop(request.composite_session_id, None)
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
if event_type == "turn.completed":
|
|
322
|
+
pending = self._pending_assistant_messages.pop(
|
|
323
|
+
request.composite_session_id, None
|
|
324
|
+
)
|
|
325
|
+
if pending:
|
|
326
|
+
pending_text, pending_parse_mode = pending
|
|
327
|
+
await self.emit_result_message(
|
|
328
|
+
request.context,
|
|
329
|
+
pending_text,
|
|
330
|
+
subtype="success",
|
|
331
|
+
started_at=request.started_at,
|
|
332
|
+
parse_mode=pending_parse_mode or "markdown",
|
|
333
|
+
)
|
|
334
|
+
else:
|
|
335
|
+
await self.emit_result_message(
|
|
336
|
+
request.context,
|
|
337
|
+
None,
|
|
338
|
+
subtype="success",
|
|
339
|
+
started_at=request.started_at,
|
|
340
|
+
parse_mode="markdown",
|
|
341
|
+
)
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
async def _delete_ack(self, request: AgentRequest):
|
|
345
|
+
ack_id = request.ack_message_id
|
|
346
|
+
if ack_id and hasattr(self.im_client, "delete_message"):
|
|
347
|
+
try:
|
|
348
|
+
await self.im_client.delete_message(
|
|
349
|
+
request.context.channel_id, ack_id
|
|
350
|
+
)
|
|
351
|
+
except Exception as err:
|
|
352
|
+
logger.debug(f"Could not delete ack message: {err}")
|
|
353
|
+
finally:
|
|
354
|
+
request.ack_message_id = None
|
|
355
|
+
|
|
356
|
+
def _prepare_last_message_payload(
|
|
357
|
+
self, text: str
|
|
358
|
+
) -> Tuple[str, Optional[str]]:
|
|
359
|
+
"""Prepare cached assistant text for reuse in result messages."""
|
|
360
|
+
return text, "markdown"
|
|
361
|
+
|
|
362
|
+
async def _notify_stream_error(self, request: AgentRequest, message: str) -> None:
|
|
363
|
+
"""Emit a notify message when Codex stdout handling fails."""
|
|
364
|
+
await self.controller.emit_agent_message(
|
|
365
|
+
request.context,
|
|
366
|
+
"notify",
|
|
367
|
+
f"⚠️ {message}\n请查看 `~/.vibe_remote/logs/vibe_remote.log` 获取更多细节。",
|
|
368
|
+
)
|