devcopilot 0.2.0__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.
- api/__init__.py +17 -0
- api/admin_config.py +1303 -0
- api/admin_routes.py +287 -0
- api/admin_static/admin.css +459 -0
- api/admin_static/admin.js +497 -0
- api/admin_static/index.html +77 -0
- api/admin_urls.py +34 -0
- api/app.py +194 -0
- api/command_utils.py +164 -0
- api/dependencies.py +144 -0
- api/detection.py +152 -0
- api/gateway_model_ids.py +54 -0
- api/model_catalog.py +133 -0
- api/model_router.py +125 -0
- api/models/__init__.py +45 -0
- api/models/anthropic.py +234 -0
- api/models/openai_responses.py +28 -0
- api/models/responses.py +60 -0
- api/optimization_handlers.py +154 -0
- api/request_pipeline.py +424 -0
- api/routes.py +156 -0
- api/runtime.py +334 -0
- api/validation_log.py +48 -0
- api/web_server_tools.py +22 -0
- api/web_tools/__init__.py +17 -0
- api/web_tools/constants.py +15 -0
- api/web_tools/egress.py +99 -0
- api/web_tools/outbound.py +278 -0
- api/web_tools/parsers.py +104 -0
- api/web_tools/request.py +87 -0
- api/web_tools/streaming.py +206 -0
- cli/__init__.py +5 -0
- cli/claude_env.py +12 -0
- cli/entrypoints.py +166 -0
- cli/env.example +209 -0
- cli/launchers/__init__.py +1 -0
- cli/launchers/claude.py +84 -0
- cli/launchers/codex.py +204 -0
- cli/launchers/codex_model_catalog.py +186 -0
- cli/launchers/common.py +93 -0
- cli/managed/__init__.py +6 -0
- cli/managed/claude.py +215 -0
- cli/managed/manager.py +157 -0
- cli/managed/session.py +260 -0
- cli/process_registry.py +78 -0
- config/__init__.py +5 -0
- config/constants.py +13 -0
- config/logging_config.py +159 -0
- config/nim.py +118 -0
- config/paths.py +91 -0
- config/provider_catalog.py +259 -0
- config/provider_ids.py +7 -0
- config/settings.py +538 -0
- core/__init__.py +1 -0
- core/anthropic/__init__.py +46 -0
- core/anthropic/content.py +31 -0
- core/anthropic/conversion.py +587 -0
- core/anthropic/emitted_sse_tracker.py +346 -0
- core/anthropic/errors.py +70 -0
- core/anthropic/native_messages_request.py +280 -0
- core/anthropic/native_sse_block_policy.py +313 -0
- core/anthropic/provider_stream_error.py +34 -0
- core/anthropic/server_tool_sse.py +14 -0
- core/anthropic/sse.py +440 -0
- core/anthropic/stream_contracts.py +205 -0
- core/anthropic/stream_recovery.py +346 -0
- core/anthropic/stream_recovery_session.py +133 -0
- core/anthropic/thinking.py +140 -0
- core/anthropic/tokens.py +117 -0
- core/anthropic/tools.py +212 -0
- core/anthropic/utils.py +9 -0
- core/openai_responses/__init__.py +5 -0
- core/openai_responses/adapter.py +31 -0
- core/openai_responses/anthropic_sse.py +59 -0
- core/openai_responses/errors.py +22 -0
- core/openai_responses/events.py +19 -0
- core/openai_responses/ids.py +21 -0
- core/openai_responses/input.py +258 -0
- core/openai_responses/items.py +37 -0
- core/openai_responses/reasoning.py +52 -0
- core/openai_responses/stream.py +25 -0
- core/openai_responses/stream_state.py +654 -0
- core/openai_responses/tools.py +374 -0
- core/openai_responses/usage.py +37 -0
- core/rate_limit.py +60 -0
- core/trace.py +216 -0
- devcopilot-0.2.0.dist-info/METADATA +687 -0
- devcopilot-0.2.0.dist-info/RECORD +189 -0
- devcopilot-0.2.0.dist-info/WHEEL +4 -0
- devcopilot-0.2.0.dist-info/entry_points.txt +6 -0
- devcopilot-0.2.0.dist-info/licenses/LICENSE +21 -0
- messaging/__init__.py +26 -0
- messaging/cli_event_constants.py +67 -0
- messaging/command_context.py +66 -0
- messaging/command_dispatcher.py +37 -0
- messaging/commands.py +275 -0
- messaging/event_parser.py +181 -0
- messaging/limiter.py +300 -0
- messaging/models.py +36 -0
- messaging/node_event_pipeline.py +127 -0
- messaging/node_runner.py +342 -0
- messaging/platforms/__init__.py +15 -0
- messaging/platforms/base.py +228 -0
- messaging/platforms/discord.py +567 -0
- messaging/platforms/factory.py +103 -0
- messaging/platforms/outbox.py +144 -0
- messaging/platforms/telegram.py +688 -0
- messaging/platforms/voice_flow.py +295 -0
- messaging/rendering/__init__.py +3 -0
- messaging/rendering/discord_markdown.py +318 -0
- messaging/rendering/markdown_tables.py +49 -0
- messaging/rendering/profiles.py +55 -0
- messaging/rendering/telegram_markdown.py +327 -0
- messaging/safe_diagnostics.py +17 -0
- messaging/session.py +334 -0
- messaging/transcript.py +581 -0
- messaging/transcription.py +164 -0
- messaging/trees/__init__.py +15 -0
- messaging/trees/data.py +482 -0
- messaging/trees/manager.py +433 -0
- messaging/trees/processor.py +179 -0
- messaging/trees/repository.py +177 -0
- messaging/turn_intake.py +235 -0
- messaging/ui_updates.py +101 -0
- messaging/voice.py +76 -0
- messaging/workflow.py +200 -0
- providers/__init__.py +31 -0
- providers/base.py +152 -0
- providers/cerebras/__init__.py +7 -0
- providers/cerebras/client.py +31 -0
- providers/cerebras/request.py +55 -0
- providers/codestral/__init__.py +7 -0
- providers/codestral/client.py +34 -0
- providers/deepseek/__init__.py +11 -0
- providers/deepseek/client.py +51 -0
- providers/deepseek/request.py +475 -0
- providers/defaults.py +41 -0
- providers/error_mapping.py +309 -0
- providers/exceptions.py +113 -0
- providers/fireworks/__init__.py +5 -0
- providers/fireworks/client.py +45 -0
- providers/fireworks/request.py +48 -0
- providers/gemini/__init__.py +7 -0
- providers/gemini/client.py +49 -0
- providers/gemini/request.py +199 -0
- providers/groq/__init__.py +7 -0
- providers/groq/client.py +31 -0
- providers/groq/request.py +83 -0
- providers/kimi/__init__.py +10 -0
- providers/kimi/client.py +53 -0
- providers/kimi/request.py +42 -0
- providers/llamacpp/__init__.py +3 -0
- providers/llamacpp/client.py +16 -0
- providers/lmstudio/__init__.py +5 -0
- providers/lmstudio/client.py +16 -0
- providers/mistral/__init__.py +7 -0
- providers/mistral/client.py +31 -0
- providers/mistral/request.py +37 -0
- providers/model_listing.py +133 -0
- providers/nvidia_nim/__init__.py +7 -0
- providers/nvidia_nim/client.py +91 -0
- providers/nvidia_nim/request.py +430 -0
- providers/nvidia_nim/voice.py +95 -0
- providers/ollama/__init__.py +7 -0
- providers/ollama/client.py +39 -0
- providers/open_router/__init__.py +7 -0
- providers/open_router/client.py +124 -0
- providers/open_router/request.py +42 -0
- providers/opencode/__init__.py +11 -0
- providers/opencode/client.py +31 -0
- providers/opencode/request.py +35 -0
- providers/rate_limit.py +300 -0
- providers/registry.py +527 -0
- providers/transports/__init__.py +1 -0
- providers/transports/anthropic_messages/__init__.py +5 -0
- providers/transports/anthropic_messages/http.py +118 -0
- providers/transports/anthropic_messages/recovery.py +206 -0
- providers/transports/anthropic_messages/stream.py +295 -0
- providers/transports/anthropic_messages/transport.py +236 -0
- providers/transports/openai_chat/__init__.py +5 -0
- providers/transports/openai_chat/recovery.py +217 -0
- providers/transports/openai_chat/stream.py +384 -0
- providers/transports/openai_chat/tool_calls.py +293 -0
- providers/transports/openai_chat/transport.py +156 -0
- providers/wafer/__init__.py +10 -0
- providers/wafer/client.py +50 -0
- providers/zai/__init__.py +10 -0
- providers/zai/client.py +46 -0
- providers/zai/request.py +42 -0
messaging/node_runner.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Run queued messaging nodes through a managed CLI session."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from core.anthropic import format_user_error_preview, get_user_facing_error_message
|
|
11
|
+
from core.trace import trace_event
|
|
12
|
+
|
|
13
|
+
from .event_parser import parse_cli_event
|
|
14
|
+
from .node_event_pipeline import handle_session_info_event, process_parsed_cli_event
|
|
15
|
+
from .platforms.base import ManagedClaudeSessionManagerProtocol, MessagingPlatform
|
|
16
|
+
from .safe_diagnostics import format_exception_for_log
|
|
17
|
+
from .session import SessionStore
|
|
18
|
+
from .transcript import RenderCtx, TranscriptBuffer
|
|
19
|
+
from .trees import MessageNode, MessageState, MessageTree, TreeQueueManager
|
|
20
|
+
from .ui_updates import ThrottledTranscriptEditor
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MessagingNodeRunner:
|
|
24
|
+
"""Owns the lifecycle of one queued messaging node."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
platform: MessagingPlatform,
|
|
30
|
+
cli_manager: ManagedClaudeSessionManagerProtocol,
|
|
31
|
+
session_store: SessionStore,
|
|
32
|
+
get_tree_queue: Callable[[], TreeQueueManager],
|
|
33
|
+
format_status: Callable[[str, str, str | None], str],
|
|
34
|
+
get_parse_mode: Callable[[], str | None],
|
|
35
|
+
get_render_ctx: Callable[[], RenderCtx],
|
|
36
|
+
get_limit_chars: Callable[[], int],
|
|
37
|
+
debug_platform_edits: bool = False,
|
|
38
|
+
debug_subagent_stack: bool = False,
|
|
39
|
+
log_raw_cli_diagnostics: bool = False,
|
|
40
|
+
log_messaging_error_details: bool = False,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.platform = platform
|
|
43
|
+
self.cli_manager = cli_manager
|
|
44
|
+
self.session_store = session_store
|
|
45
|
+
self._get_tree_queue = get_tree_queue
|
|
46
|
+
self._format_status = format_status
|
|
47
|
+
self._get_parse_mode = get_parse_mode
|
|
48
|
+
self._get_render_ctx = get_render_ctx
|
|
49
|
+
self._get_limit_chars = get_limit_chars
|
|
50
|
+
self._debug_platform_edits = debug_platform_edits
|
|
51
|
+
self._debug_subagent_stack = debug_subagent_stack
|
|
52
|
+
self._log_raw_cli_diagnostics = log_raw_cli_diagnostics
|
|
53
|
+
self._log_messaging_error_details = log_messaging_error_details
|
|
54
|
+
|
|
55
|
+
def _create_transcript_and_render_ctx(
|
|
56
|
+
self,
|
|
57
|
+
) -> tuple[TranscriptBuffer, RenderCtx]:
|
|
58
|
+
"""Create transcript buffer and render context for node processing."""
|
|
59
|
+
transcript = TranscriptBuffer(
|
|
60
|
+
show_tool_results=False,
|
|
61
|
+
debug_subagent_stack=self._debug_subagent_stack,
|
|
62
|
+
)
|
|
63
|
+
return transcript, self._get_render_ctx()
|
|
64
|
+
|
|
65
|
+
def _save_tree(self, tree: MessageTree | None) -> None:
|
|
66
|
+
"""Persist tree state after runner-owned mutations."""
|
|
67
|
+
if tree:
|
|
68
|
+
self.session_store.save_tree(tree.root_id, tree.to_dict())
|
|
69
|
+
|
|
70
|
+
async def process_node(
|
|
71
|
+
self,
|
|
72
|
+
node_id: str,
|
|
73
|
+
node: MessageNode,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Core task processor for a single CLI interaction."""
|
|
76
|
+
incoming = node.incoming
|
|
77
|
+
status_msg_id = node.status_message_id
|
|
78
|
+
chat_id = incoming.chat_id
|
|
79
|
+
|
|
80
|
+
with logger.contextualize(node_id=node_id, chat_id=chat_id):
|
|
81
|
+
await self._process_node_impl(node_id, node, chat_id, status_msg_id)
|
|
82
|
+
|
|
83
|
+
async def _process_node_impl(
|
|
84
|
+
self,
|
|
85
|
+
node_id: str,
|
|
86
|
+
node: MessageNode,
|
|
87
|
+
chat_id: str,
|
|
88
|
+
status_msg_id: str,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Internal implementation of process_node with context bound."""
|
|
91
|
+
incoming = node.incoming
|
|
92
|
+
|
|
93
|
+
tree_queue = self._get_tree_queue()
|
|
94
|
+
tree = tree_queue.get_tree_for_node(node_id)
|
|
95
|
+
if tree:
|
|
96
|
+
await tree.update_state(node_id, MessageState.IN_PROGRESS)
|
|
97
|
+
|
|
98
|
+
transcript, render_ctx = self._create_transcript_and_render_ctx()
|
|
99
|
+
|
|
100
|
+
had_transcript_events = False
|
|
101
|
+
captured_session_id = None
|
|
102
|
+
temp_session_id = None
|
|
103
|
+
last_status: str | None = None
|
|
104
|
+
|
|
105
|
+
parent_session_id = None
|
|
106
|
+
platform_nm = getattr(self.platform, "name", "messaging")
|
|
107
|
+
if tree and node.parent_id:
|
|
108
|
+
parent_session_id = tree.get_parent_session_id(node_id)
|
|
109
|
+
if parent_session_id:
|
|
110
|
+
trace_event(
|
|
111
|
+
stage="claude_cli",
|
|
112
|
+
event="claude_cli.fork.from_parent_session",
|
|
113
|
+
source=platform_nm,
|
|
114
|
+
chat_id=chat_id,
|
|
115
|
+
node_id=node_id,
|
|
116
|
+
parent_session_id=parent_session_id,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
editor = ThrottledTranscriptEditor(
|
|
120
|
+
platform=self.platform,
|
|
121
|
+
parse_mode=self._get_parse_mode(),
|
|
122
|
+
get_limit_chars=self._get_limit_chars,
|
|
123
|
+
transcript=transcript,
|
|
124
|
+
render_ctx=render_ctx,
|
|
125
|
+
node_id=node_id,
|
|
126
|
+
chat_id=chat_id,
|
|
127
|
+
status_msg_id=status_msg_id,
|
|
128
|
+
debug_platform_edits=self._debug_platform_edits,
|
|
129
|
+
log_messaging_error_details=self._log_messaging_error_details,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async def update_ui(status: str | None = None, force: bool = False) -> None:
|
|
133
|
+
await editor.update(status, force=force)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
try:
|
|
137
|
+
(
|
|
138
|
+
cli_session,
|
|
139
|
+
session_or_temp_id,
|
|
140
|
+
is_new,
|
|
141
|
+
) = await self.cli_manager.get_or_create_session(
|
|
142
|
+
session_id=parent_session_id
|
|
143
|
+
)
|
|
144
|
+
if is_new:
|
|
145
|
+
temp_session_id = session_or_temp_id
|
|
146
|
+
else:
|
|
147
|
+
captured_session_id = session_or_temp_id
|
|
148
|
+
|
|
149
|
+
sess_evt = (
|
|
150
|
+
"claude_cli.session.pending_created"
|
|
151
|
+
if is_new
|
|
152
|
+
else "claude_cli.session.reused"
|
|
153
|
+
)
|
|
154
|
+
trace_event(
|
|
155
|
+
stage="claude_cli",
|
|
156
|
+
event=sess_evt,
|
|
157
|
+
source=platform_nm,
|
|
158
|
+
chat_id=chat_id,
|
|
159
|
+
node_id=node_id,
|
|
160
|
+
status_message_id=status_msg_id,
|
|
161
|
+
session_handle=str(session_or_temp_id),
|
|
162
|
+
parent_resume_session_id=parent_session_id,
|
|
163
|
+
fork_requested=bool(parent_session_id),
|
|
164
|
+
)
|
|
165
|
+
trace_event(
|
|
166
|
+
stage="claude_cli",
|
|
167
|
+
event="claude_cli.request.sent",
|
|
168
|
+
source=platform_nm,
|
|
169
|
+
chat_id=chat_id,
|
|
170
|
+
node_id=node_id,
|
|
171
|
+
prompt=incoming.text,
|
|
172
|
+
fork_session_arg=bool(parent_session_id),
|
|
173
|
+
resume_session_arg=parent_session_id,
|
|
174
|
+
)
|
|
175
|
+
except RuntimeError as e:
|
|
176
|
+
error_message = get_user_facing_error_message(e)
|
|
177
|
+
transcript.apply({"type": "error", "message": error_message})
|
|
178
|
+
await update_ui(
|
|
179
|
+
self._format_status("⏳", "Session limit reached", None),
|
|
180
|
+
force=True,
|
|
181
|
+
)
|
|
182
|
+
if tree:
|
|
183
|
+
await tree.update_state(
|
|
184
|
+
node_id,
|
|
185
|
+
MessageState.ERROR,
|
|
186
|
+
error_message=error_message,
|
|
187
|
+
)
|
|
188
|
+
self._save_tree(tree)
|
|
189
|
+
trace_event(
|
|
190
|
+
stage="claude_cli",
|
|
191
|
+
event="claude_cli.session.limit_reached",
|
|
192
|
+
source=platform_nm,
|
|
193
|
+
chat_id=chat_id,
|
|
194
|
+
node_id=node_id,
|
|
195
|
+
)
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
async for event_data in cli_session.start_task(
|
|
199
|
+
incoming.text,
|
|
200
|
+
session_id=parent_session_id,
|
|
201
|
+
fork_session=bool(parent_session_id),
|
|
202
|
+
):
|
|
203
|
+
if not isinstance(event_data, dict):
|
|
204
|
+
logger.warning(
|
|
205
|
+
f"HANDLER: Non-dict event received: {type(event_data)}"
|
|
206
|
+
)
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
(
|
|
210
|
+
captured_session_id,
|
|
211
|
+
temp_session_id,
|
|
212
|
+
) = await handle_session_info_event(
|
|
213
|
+
event_data,
|
|
214
|
+
tree,
|
|
215
|
+
node_id,
|
|
216
|
+
captured_session_id,
|
|
217
|
+
temp_session_id,
|
|
218
|
+
cli_manager=self.cli_manager,
|
|
219
|
+
session_store=self.session_store,
|
|
220
|
+
)
|
|
221
|
+
if event_data.get("type") == "session_info":
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
parsed_list = parse_cli_event(
|
|
225
|
+
event_data, log_raw_cli=self._log_raw_cli_diagnostics
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
for parsed in parsed_list:
|
|
229
|
+
(
|
|
230
|
+
last_status,
|
|
231
|
+
had_transcript_events,
|
|
232
|
+
) = await process_parsed_cli_event(
|
|
233
|
+
parsed,
|
|
234
|
+
transcript,
|
|
235
|
+
update_ui,
|
|
236
|
+
last_status,
|
|
237
|
+
had_transcript_events,
|
|
238
|
+
tree,
|
|
239
|
+
node_id,
|
|
240
|
+
captured_session_id,
|
|
241
|
+
session_store=self.session_store,
|
|
242
|
+
format_status=self._format_status,
|
|
243
|
+
propagate_error_to_children=self.propagate_error_to_children,
|
|
244
|
+
log_messaging_error_details=self._log_messaging_error_details,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
except asyncio.CancelledError:
|
|
248
|
+
trace_event(
|
|
249
|
+
stage="claude_cli",
|
|
250
|
+
event="turn.processor.cancelled",
|
|
251
|
+
source=platform_nm,
|
|
252
|
+
chat_id=chat_id,
|
|
253
|
+
node_id=node_id,
|
|
254
|
+
)
|
|
255
|
+
logger.warning(f"HANDLER: Task cancelled for node {node_id}")
|
|
256
|
+
cancel_reason = None
|
|
257
|
+
if isinstance(node.context, dict):
|
|
258
|
+
cancel_reason = node.context.get("cancel_reason")
|
|
259
|
+
|
|
260
|
+
if cancel_reason == "stop":
|
|
261
|
+
await update_ui(self._format_status("⏹", "Stopped.", None), force=True)
|
|
262
|
+
else:
|
|
263
|
+
transcript.apply({"type": "error", "message": "Task was cancelled"})
|
|
264
|
+
await update_ui(
|
|
265
|
+
self._format_status("❌", "Cancelled", None), force=True
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if tree:
|
|
269
|
+
await tree.update_state(
|
|
270
|
+
node_id, MessageState.ERROR, error_message="Cancelled by user"
|
|
271
|
+
)
|
|
272
|
+
self._save_tree(tree)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
trace_event(
|
|
275
|
+
stage="claude_cli",
|
|
276
|
+
event="turn.processor.exception",
|
|
277
|
+
source=platform_nm,
|
|
278
|
+
chat_id=chat_id,
|
|
279
|
+
node_id=node_id,
|
|
280
|
+
exc_type=type(e).__name__,
|
|
281
|
+
)
|
|
282
|
+
logger.error(
|
|
283
|
+
"HANDLER: Task failed with exception: {}",
|
|
284
|
+
format_exception_for_log(
|
|
285
|
+
e, log_full_message=self._log_messaging_error_details
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
error_msg = format_user_error_preview(e)
|
|
289
|
+
transcript.apply({"type": "error", "message": error_msg})
|
|
290
|
+
await update_ui(self._format_status("💥", "Task Failed", None), force=True)
|
|
291
|
+
if tree:
|
|
292
|
+
await self.propagate_error_to_children(
|
|
293
|
+
node_id, error_msg, "Parent task failed"
|
|
294
|
+
)
|
|
295
|
+
finally:
|
|
296
|
+
trace_event(
|
|
297
|
+
stage="routing",
|
|
298
|
+
event="turn.processor.finished",
|
|
299
|
+
source=platform_nm,
|
|
300
|
+
chat_id=chat_id,
|
|
301
|
+
node_id=node_id,
|
|
302
|
+
claude_session_id=captured_session_id or temp_session_id,
|
|
303
|
+
)
|
|
304
|
+
try:
|
|
305
|
+
if captured_session_id:
|
|
306
|
+
await self.cli_manager.remove_session(captured_session_id)
|
|
307
|
+
elif temp_session_id:
|
|
308
|
+
await self.cli_manager.remove_session(temp_session_id)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.debug(
|
|
311
|
+
"Failed to remove session for node {}: {}",
|
|
312
|
+
node_id,
|
|
313
|
+
format_exception_for_log(
|
|
314
|
+
e, log_full_message=self._log_messaging_error_details
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
async def propagate_error_to_children(
|
|
319
|
+
self,
|
|
320
|
+
node_id: str,
|
|
321
|
+
error_msg: str,
|
|
322
|
+
child_status_text: str,
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Mark node as error and propagate to pending children with UI updates."""
|
|
325
|
+
tree_queue = self._get_tree_queue()
|
|
326
|
+
affected = await tree_queue.mark_node_error(
|
|
327
|
+
node_id, error_msg, propagate_to_children=True
|
|
328
|
+
)
|
|
329
|
+
if affected:
|
|
330
|
+
self._save_tree(tree_queue.get_tree_for_node(node_id))
|
|
331
|
+
for child in affected[1:]:
|
|
332
|
+
self.platform.fire_and_forget(
|
|
333
|
+
self.platform.queue_edit_message(
|
|
334
|
+
child.incoming.chat_id,
|
|
335
|
+
child.status_message_id,
|
|
336
|
+
self._format_status("❌", "Cancelled:", child_status_text),
|
|
337
|
+
parse_mode=self._get_parse_mode(),
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
__all__ = ["MessagingNodeRunner"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Messaging platform adapters (Telegram, Discord, etc.)."""
|
|
2
|
+
|
|
3
|
+
from .base import (
|
|
4
|
+
ManagedClaudeSessionManagerProtocol,
|
|
5
|
+
ManagedClaudeSessionProtocol,
|
|
6
|
+
MessagingPlatform,
|
|
7
|
+
)
|
|
8
|
+
from .factory import create_messaging_platform
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ManagedClaudeSessionManagerProtocol",
|
|
12
|
+
"ManagedClaudeSessionProtocol",
|
|
13
|
+
"MessagingPlatform",
|
|
14
|
+
"create_messaging_platform",
|
|
15
|
+
]
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Abstract base class for messaging platforms."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
Protocol,
|
|
8
|
+
runtime_checkable,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from ..models import IncomingMessage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class ManagedClaudeSessionProtocol(Protocol):
|
|
16
|
+
"""Protocol for managed Claude sessions - avoid circular imports."""
|
|
17
|
+
|
|
18
|
+
def start_task(
|
|
19
|
+
self, prompt: str, session_id: str | None = None, fork_session: bool = False
|
|
20
|
+
) -> AsyncGenerator[dict, Any]: ...
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def is_busy(self) -> bool: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@runtime_checkable
|
|
27
|
+
class ManagedClaudeSessionManagerProtocol(Protocol):
|
|
28
|
+
"""
|
|
29
|
+
Protocol for managed Claude session managers to avoid tight coupling.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
async def get_or_create_session(
|
|
33
|
+
self, session_id: str | None = None
|
|
34
|
+
) -> tuple[ManagedClaudeSessionProtocol, str, bool]:
|
|
35
|
+
"""
|
|
36
|
+
Get an existing session or create a new one.
|
|
37
|
+
|
|
38
|
+
Returns: Tuple of (session, session_id, is_new_session)
|
|
39
|
+
"""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
async def register_real_session_id(
|
|
43
|
+
self, temp_id: str, real_session_id: str
|
|
44
|
+
) -> bool:
|
|
45
|
+
"""Register the real session ID from CLI output."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
async def stop_all(self) -> None:
|
|
49
|
+
"""Stop all sessions."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
async def remove_session(self, session_id: str) -> bool:
|
|
53
|
+
"""Remove a session from the manager."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
def get_stats(self) -> dict:
|
|
57
|
+
"""Get session statistics."""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class MessagingPlatform(ABC):
|
|
62
|
+
"""
|
|
63
|
+
Base class for all messaging platform adapters.
|
|
64
|
+
|
|
65
|
+
Implement this to add support for Telegram, Discord, Slack, etc.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
name: str = "base"
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
async def start(self) -> None:
|
|
72
|
+
"""Initialize and connect to the messaging platform."""
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
async def stop(self) -> None:
|
|
77
|
+
"""Disconnect and cleanup resources."""
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
async def send_message(
|
|
82
|
+
self,
|
|
83
|
+
chat_id: str,
|
|
84
|
+
text: str,
|
|
85
|
+
reply_to: str | None = None,
|
|
86
|
+
parse_mode: str | None = None,
|
|
87
|
+
message_thread_id: str | None = None,
|
|
88
|
+
) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Send a message to a chat.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
chat_id: The chat/channel ID to send to
|
|
94
|
+
text: Message content
|
|
95
|
+
reply_to: Optional message ID to reply to
|
|
96
|
+
parse_mode: Optional formatting mode ("markdown", "html")
|
|
97
|
+
message_thread_id: Optional thread or topic id for threaded channels
|
|
98
|
+
(e.g. forum topics); unused on platforms that do not support it.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The message ID of the sent message
|
|
102
|
+
"""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
async def edit_message(
|
|
107
|
+
self,
|
|
108
|
+
chat_id: str,
|
|
109
|
+
message_id: str,
|
|
110
|
+
text: str,
|
|
111
|
+
parse_mode: str | None = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Edit an existing message.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
chat_id: The chat/channel ID
|
|
118
|
+
message_id: The message ID to edit
|
|
119
|
+
text: New message content
|
|
120
|
+
parse_mode: Optional formatting mode
|
|
121
|
+
"""
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
async def delete_message(
|
|
126
|
+
self,
|
|
127
|
+
chat_id: str,
|
|
128
|
+
message_id: str,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Delete a message from a chat.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
chat_id: The chat/channel ID
|
|
135
|
+
message_id: The message ID to delete
|
|
136
|
+
"""
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
@abstractmethod
|
|
140
|
+
async def queue_send_message(
|
|
141
|
+
self,
|
|
142
|
+
chat_id: str,
|
|
143
|
+
text: str,
|
|
144
|
+
reply_to: str | None = None,
|
|
145
|
+
parse_mode: str | None = None,
|
|
146
|
+
fire_and_forget: bool = True,
|
|
147
|
+
message_thread_id: str | None = None,
|
|
148
|
+
) -> str | None:
|
|
149
|
+
"""
|
|
150
|
+
Enqueue a message to be sent.
|
|
151
|
+
|
|
152
|
+
If fire_and_forget is True, returns None immediately.
|
|
153
|
+
Otherwise, waits for the rate limiter and returns message ID.
|
|
154
|
+
"""
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
@abstractmethod
|
|
158
|
+
async def queue_edit_message(
|
|
159
|
+
self,
|
|
160
|
+
chat_id: str,
|
|
161
|
+
message_id: str,
|
|
162
|
+
text: str,
|
|
163
|
+
parse_mode: str | None = None,
|
|
164
|
+
fire_and_forget: bool = True,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""
|
|
167
|
+
Enqueue a message edit.
|
|
168
|
+
|
|
169
|
+
If fire_and_forget is True, returns immediately.
|
|
170
|
+
Otherwise, waits for the rate limiter.
|
|
171
|
+
"""
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
@abstractmethod
|
|
175
|
+
async def queue_delete_message(
|
|
176
|
+
self,
|
|
177
|
+
chat_id: str,
|
|
178
|
+
message_id: str,
|
|
179
|
+
fire_and_forget: bool = True,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""
|
|
182
|
+
Enqueue a message deletion.
|
|
183
|
+
|
|
184
|
+
If fire_and_forget is True, returns immediately.
|
|
185
|
+
Otherwise, waits for the rate limiter.
|
|
186
|
+
"""
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
async def queue_delete_messages(
|
|
190
|
+
self,
|
|
191
|
+
chat_id: str,
|
|
192
|
+
message_ids: list[str],
|
|
193
|
+
*,
|
|
194
|
+
fire_and_forget: bool = True,
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Delete many messages; default loops :meth:`queue_delete_message`.
|
|
197
|
+
|
|
198
|
+
Adapters with native bulk delete should override.
|
|
199
|
+
"""
|
|
200
|
+
for mid in message_ids:
|
|
201
|
+
await self.queue_delete_message(
|
|
202
|
+
chat_id, mid, fire_and_forget=fire_and_forget
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@abstractmethod
|
|
206
|
+
def on_message(
|
|
207
|
+
self,
|
|
208
|
+
handler: Callable[[IncomingMessage], Awaitable[None]],
|
|
209
|
+
) -> None:
|
|
210
|
+
"""
|
|
211
|
+
Register a message handler callback.
|
|
212
|
+
|
|
213
|
+
The handler will be called for each incoming message.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
handler: Async function that processes incoming messages
|
|
217
|
+
"""
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
@abstractmethod
|
|
221
|
+
def fire_and_forget(self, task: Awaitable[Any]) -> None:
|
|
222
|
+
"""Execute a coroutine without awaiting it."""
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def is_connected(self) -> bool:
|
|
227
|
+
"""Check if the platform is connected."""
|
|
228
|
+
return False
|