agentpool 2.2.3__py3-none-any.whl → 2.5.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.
- acp/__init__.py +0 -4
- acp/acp_requests.py +20 -77
- acp/agent/connection.py +8 -0
- acp/agent/implementations/debug_server/debug_server.py +6 -2
- acp/agent/protocol.py +6 -0
- acp/client/connection.py +38 -29
- acp/client/implementations/default_client.py +3 -2
- acp/client/implementations/headless_client.py +2 -2
- acp/connection.py +2 -2
- acp/notifications.py +18 -49
- acp/schema/__init__.py +2 -0
- acp/schema/agent_responses.py +21 -0
- acp/schema/client_requests.py +3 -3
- acp/schema/session_state.py +63 -29
- acp/task/supervisor.py +2 -2
- acp/utils.py +2 -2
- agentpool/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +278 -263
- agentpool/agents/acp_agent/acp_converters.py +150 -17
- agentpool/agents/acp_agent/client_handler.py +35 -24
- agentpool/agents/acp_agent/session_state.py +14 -6
- agentpool/agents/agent.py +471 -643
- agentpool/agents/agui_agent/agui_agent.py +104 -107
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +485 -32
- agentpool/agents/claude_code_agent/FORKING.md +191 -0
- agentpool/agents/claude_code_agent/__init__.py +13 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +654 -334
- agentpool/agents/claude_code_agent/converters.py +4 -141
- agentpool/agents/claude_code_agent/models.py +77 -0
- agentpool/agents/claude_code_agent/static_info.py +100 -0
- agentpool/agents/claude_code_agent/usage.py +242 -0
- agentpool/agents/events/__init__.py +22 -0
- agentpool/agents/events/builtin_handlers.py +65 -0
- agentpool/agents/events/event_emitter.py +3 -0
- agentpool/agents/events/events.py +84 -3
- agentpool/agents/events/infer_info.py +145 -0
- agentpool/agents/events/processors.py +254 -0
- agentpool/agents/interactions.py +41 -6
- agentpool/agents/modes.py +13 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +35 -21
- agentpool/config_resources/acp_assistant.yml +2 -2
- agentpool/config_resources/agents.yml +3 -0
- agentpool/config_resources/agents_template.yml +1 -0
- agentpool/config_resources/claude_code_agent.yml +9 -8
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +104 -265
- agentpool/delegation/team.py +57 -57
- agentpool/delegation/teamrun.py +50 -55
- agentpool/functional/run.py +10 -4
- agentpool/mcp_server/client.py +73 -38
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +9 -23
- agentpool/mcp_server/registries/official_registry_client.py +10 -1
- agentpool/mcp_server/tool_bridge.py +114 -79
- agentpool/messaging/connection_manager.py +11 -10
- agentpool/messaging/event_manager.py +5 -5
- agentpool/messaging/message_container.py +6 -30
- agentpool/messaging/message_history.py +87 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +2 -26
- agentpool/messaging/processing.py +10 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -2
- agentpool/models/acp_agents/mcp_capable.py +124 -15
- agentpool/models/acp_agents/non_mcp.py +0 -23
- agentpool/models/agents.py +66 -66
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +111 -17
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +70 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/resource_providers/__init__.py +2 -0
- agentpool/resource_providers/aggregating.py +4 -2
- agentpool/resource_providers/base.py +13 -3
- agentpool/resource_providers/codemode/code_executor.py +72 -5
- agentpool/resource_providers/codemode/helpers.py +2 -2
- agentpool/resource_providers/codemode/provider.py +64 -12
- agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
- agentpool/resource_providers/codemode/remote_provider.py +9 -12
- agentpool/resource_providers/filtering.py +3 -1
- agentpool/resource_providers/mcp_provider.py +66 -12
- agentpool/resource_providers/plan_provider.py +111 -18
- agentpool/resource_providers/pool.py +5 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +2 -2
- agentpool/sessions/__init__.py +2 -0
- agentpool/sessions/manager.py +2 -3
- agentpool/sessions/models.py +9 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/storage/manager.py +361 -54
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +1 -1
- agentpool/tool_impls/__init__.py +6 -0
- agentpool/tool_impls/agent_cli/__init__.py +42 -0
- agentpool/tool_impls/agent_cli/tool.py +95 -0
- agentpool/tool_impls/bash/__init__.py +64 -0
- agentpool/tool_impls/bash/helpers.py +35 -0
- agentpool/tool_impls/bash/tool.py +171 -0
- agentpool/tool_impls/delete_path/__init__.py +70 -0
- agentpool/tool_impls/delete_path/tool.py +142 -0
- agentpool/tool_impls/download_file/__init__.py +80 -0
- agentpool/tool_impls/download_file/tool.py +183 -0
- agentpool/tool_impls/execute_code/__init__.py +55 -0
- agentpool/tool_impls/execute_code/tool.py +163 -0
- agentpool/tool_impls/grep/__init__.py +80 -0
- agentpool/tool_impls/grep/tool.py +200 -0
- agentpool/tool_impls/list_directory/__init__.py +73 -0
- agentpool/tool_impls/list_directory/tool.py +197 -0
- agentpool/tool_impls/question/__init__.py +42 -0
- agentpool/tool_impls/question/tool.py +127 -0
- agentpool/tool_impls/read/__init__.py +104 -0
- agentpool/tool_impls/read/tool.py +305 -0
- agentpool/tools/__init__.py +2 -1
- agentpool/tools/base.py +114 -34
- agentpool/tools/manager.py +57 -1
- agentpool/ui/base.py +2 -2
- agentpool/ui/mock_provider.py +2 -2
- agentpool/ui/stdlib_provider.py +2 -2
- agentpool/utils/streams.py +21 -96
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/METADATA +16 -22
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/RECORD +242 -195
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +20 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +59 -1
- agentpool_cli/serve_opencode.py +1 -1
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +12 -5
- agentpool_commands/agents.py +1 -1
- agentpool_commands/pool.py +260 -0
- agentpool_commands/session.py +1 -1
- agentpool_commands/text_sharing/__init__.py +119 -0
- agentpool_commands/text_sharing/base.py +123 -0
- agentpool_commands/text_sharing/github_gist.py +80 -0
- agentpool_commands/text_sharing/opencode.py +462 -0
- agentpool_commands/text_sharing/paste_rs.py +59 -0
- agentpool_commands/text_sharing/pastebin.py +116 -0
- agentpool_commands/text_sharing/shittycodingagent.py +112 -0
- agentpool_commands/utils.py +31 -32
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/converters.py +1 -1
- agentpool_config/event_handlers.py +42 -0
- agentpool_config/events.py +1 -1
- agentpool_config/forward_targets.py +1 -4
- agentpool_config/jinja.py +3 -3
- agentpool_config/mcp_server.py +1 -5
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +38 -39
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -28
- agentpool_config/toolsets.py +22 -90
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +125 -56
- agentpool_server/acp_server/commands/acp_commands.py +46 -216
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +8 -7
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +1 -11
- agentpool_server/acp_server/session.py +90 -410
- agentpool_server/acp_server/session_manager.py +8 -34
- agentpool_server/agui_server/server.py +3 -1
- agentpool_server/mcp_server/server.py +5 -2
- agentpool_server/opencode_server/ENDPOINTS.md +53 -14
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +0 -8
- agentpool_server/opencode_server/converters.py +132 -26
- agentpool_server/opencode_server/input_provider.py +160 -8
- agentpool_server/opencode_server/models/__init__.py +42 -20
- agentpool_server/opencode_server/models/app.py +12 -0
- agentpool_server/opencode_server/models/events.py +203 -29
- agentpool_server/opencode_server/models/mcp.py +19 -0
- agentpool_server/opencode_server/models/message.py +18 -1
- agentpool_server/opencode_server/models/parts.py +134 -1
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +13 -1
- agentpool_server/opencode_server/routes/__init__.py +4 -0
- agentpool_server/opencode_server/routes/agent_routes.py +33 -2
- agentpool_server/opencode_server/routes/app_routes.py +66 -3
- agentpool_server/opencode_server/routes/config_routes.py +66 -5
- agentpool_server/opencode_server/routes/file_routes.py +184 -5
- agentpool_server/opencode_server/routes/global_routes.py +1 -1
- agentpool_server/opencode_server/routes/lsp_routes.py +1 -1
- agentpool_server/opencode_server/routes/message_routes.py +122 -66
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +23 -22
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +139 -68
- agentpool_server/opencode_server/routes/tui_routes.py +1 -1
- agentpool_server/opencode_server/server.py +47 -2
- agentpool_server/opencode_server/state.py +30 -0
- agentpool_storage/__init__.py +0 -4
- agentpool_storage/base.py +81 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/{claude_provider.py → claude_provider/provider.py} +190 -8
- agentpool_storage/file_provider.py +149 -15
- agentpool_storage/memory_provider.py +132 -12
- agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
- agentpool_storage/opencode_provider/__init__.py +16 -0
- agentpool_storage/opencode_provider/helpers.py +414 -0
- agentpool_storage/opencode_provider/provider.py +895 -0
- agentpool_storage/session_store.py +20 -6
- agentpool_storage/sql_provider/sql_provider.py +135 -2
- agentpool_storage/sql_provider/utils.py +2 -12
- agentpool_storage/zed_provider/__init__.py +16 -0
- agentpool_storage/zed_provider/helpers.py +281 -0
- agentpool_storage/zed_provider/models.py +130 -0
- agentpool_storage/zed_provider/provider.py +442 -0
- agentpool_storage/zed_provider.py +803 -0
- agentpool_toolsets/__init__.py +0 -2
- agentpool_toolsets/builtin/__init__.py +2 -4
- agentpool_toolsets/builtin/code.py +4 -4
- agentpool_toolsets/builtin/debug.py +115 -40
- agentpool_toolsets/builtin/execution_environment.py +54 -165
- agentpool_toolsets/builtin/skills.py +0 -77
- agentpool_toolsets/builtin/subagent_tools.py +64 -51
- agentpool_toolsets/builtin/workers.py +4 -2
- agentpool_toolsets/composio_toolset.py +2 -2
- agentpool_toolsets/entry_points.py +3 -1
- agentpool_toolsets/fsspec_toolset/grep.py +25 -5
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/toolset.py +350 -66
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +74 -17
- agentpool_toolsets/mcp_run_toolset.py +8 -11
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool_config/resources.py +0 -33
- agentpool_server/acp_server/acp_tools.py +0 -43
- agentpool_server/acp_server/commands/spawn.py +0 -210
- agentpool_storage/opencode_provider.py +0 -730
- agentpool_storage/text_log_provider.py +0 -276
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.2.3.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
|
-
from fastapi import APIRouter, HTTPException, Query
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Query, status
|
|
8
8
|
from pydantic_ai import FunctionToolCallEvent
|
|
9
9
|
from pydantic_ai.messages import (
|
|
10
10
|
PartDeltaEvent,
|
|
@@ -14,18 +14,18 @@ from pydantic_ai.messages import (
|
|
|
14
14
|
ToolCallPart as PydanticToolCallPart,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
from agentpool.agents.claude_code_agent.converters import derive_rich_tool_info
|
|
18
17
|
from agentpool.agents.events import (
|
|
19
18
|
CompactionEvent,
|
|
20
19
|
FileContentItem,
|
|
21
20
|
LocationContentItem,
|
|
22
21
|
StreamCompleteEvent,
|
|
22
|
+
SubAgentEvent,
|
|
23
23
|
TextContentItem,
|
|
24
24
|
ToolCallCompleteEvent,
|
|
25
25
|
ToolCallProgressEvent,
|
|
26
26
|
ToolCallStartEvent,
|
|
27
27
|
)
|
|
28
|
-
from agentpool.
|
|
28
|
+
from agentpool.agents.events.infer_info import derive_rich_tool_info
|
|
29
29
|
from agentpool.utils import identifiers as identifier
|
|
30
30
|
from agentpool.utils.pydantic_ai_helpers import safe_args_as_dict
|
|
31
31
|
from agentpool_server.opencode_server.converters import (
|
|
@@ -33,22 +33,20 @@ from agentpool_server.opencode_server.converters import (
|
|
|
33
33
|
extract_user_prompt_from_parts,
|
|
34
34
|
opencode_to_chat_message,
|
|
35
35
|
)
|
|
36
|
-
from agentpool_server.opencode_server.dependencies import StateDep
|
|
37
|
-
from agentpool_server.opencode_server.models import (
|
|
36
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
37
|
+
from agentpool_server.opencode_server.models import (
|
|
38
38
|
AssistantMessage,
|
|
39
39
|
MessagePath,
|
|
40
40
|
MessageRequest,
|
|
41
41
|
MessageTime,
|
|
42
42
|
MessageUpdatedEvent,
|
|
43
43
|
MessageWithParts,
|
|
44
|
-
Part,
|
|
45
44
|
PartUpdatedEvent,
|
|
46
45
|
SessionCompactedEvent,
|
|
47
46
|
SessionErrorEvent,
|
|
48
47
|
SessionIdleEvent,
|
|
49
48
|
SessionStatus,
|
|
50
49
|
SessionStatusEvent,
|
|
51
|
-
SessionUpdatedEvent,
|
|
52
50
|
StepFinishPart,
|
|
53
51
|
StepStartPart,
|
|
54
52
|
TextPart,
|
|
@@ -71,14 +69,14 @@ from agentpool_server.opencode_server.models.parts import (
|
|
|
71
69
|
TimeStartEndOptional,
|
|
72
70
|
TokenCache,
|
|
73
71
|
)
|
|
74
|
-
from agentpool_server.opencode_server.routes.session_routes import
|
|
75
|
-
get_or_load_session,
|
|
76
|
-
opencode_to_session_data,
|
|
77
|
-
)
|
|
72
|
+
from agentpool_server.opencode_server.routes.session_routes import get_or_load_session
|
|
78
73
|
from agentpool_server.opencode_server.time_utils import now_ms
|
|
79
74
|
|
|
80
75
|
|
|
81
76
|
if TYPE_CHECKING:
|
|
77
|
+
from agentpool_server.opencode_server.models import (
|
|
78
|
+
Part,
|
|
79
|
+
)
|
|
82
80
|
from agentpool_server.opencode_server.state import ServerState
|
|
83
81
|
|
|
84
82
|
|
|
@@ -172,48 +170,6 @@ async def persist_message_to_storage(
|
|
|
172
170
|
pass
|
|
173
171
|
|
|
174
172
|
|
|
175
|
-
async def _generate_session_title(
|
|
176
|
-
state: ServerState,
|
|
177
|
-
session_id: str,
|
|
178
|
-
user_prompt: str,
|
|
179
|
-
assistant_response: str,
|
|
180
|
-
) -> None:
|
|
181
|
-
"""Generate a title for the session in the background."""
|
|
182
|
-
try:
|
|
183
|
-
if not state.pool.storage:
|
|
184
|
-
return
|
|
185
|
-
|
|
186
|
-
# Create ChatMessage objects for the title generator
|
|
187
|
-
messages = [
|
|
188
|
-
ChatMessage[str](role="user", content=user_prompt),
|
|
189
|
-
ChatMessage[str](role="assistant", content=assistant_response),
|
|
190
|
-
]
|
|
191
|
-
|
|
192
|
-
# Generate title using storage manager
|
|
193
|
-
title = await state.pool.storage.generate_conversation_title(
|
|
194
|
-
messages=messages,
|
|
195
|
-
conversation_id=session_id,
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
if title and session_id in state.sessions:
|
|
199
|
-
# Update session with new title
|
|
200
|
-
session = state.sessions[session_id]
|
|
201
|
-
updated_session = session.model_copy(update={"title": title})
|
|
202
|
-
state.sessions[session_id] = updated_session
|
|
203
|
-
session_data = opencode_to_session_data(
|
|
204
|
-
updated_session,
|
|
205
|
-
agent_name=state.agent.name,
|
|
206
|
-
pool_id=state.pool.manifest.config_file_path,
|
|
207
|
-
)
|
|
208
|
-
await state.pool.sessions.store.save(session_data)
|
|
209
|
-
|
|
210
|
-
# Broadcast session update
|
|
211
|
-
await state.broadcast_event(SessionUpdatedEvent.create(updated_session))
|
|
212
|
-
except Exception: # noqa: BLE001
|
|
213
|
-
# Don't fail if title generation fails
|
|
214
|
-
pass
|
|
215
|
-
|
|
216
|
-
|
|
217
173
|
router = APIRouter(prefix="/session/{session_id}", tags=["message"])
|
|
218
174
|
|
|
219
175
|
|
|
@@ -234,13 +190,16 @@ async def list_messages(
|
|
|
234
190
|
return messages
|
|
235
191
|
|
|
236
192
|
|
|
237
|
-
|
|
238
|
-
async def send_message( # noqa: PLR0915
|
|
193
|
+
async def _process_message( # noqa: PLR0915
|
|
239
194
|
session_id: str,
|
|
240
195
|
request: MessageRequest,
|
|
241
196
|
state: StateDep,
|
|
242
197
|
) -> MessageWithParts:
|
|
243
|
-
"""
|
|
198
|
+
"""Internal helper to process a message request.
|
|
199
|
+
|
|
200
|
+
This does the actual work of creating messages, running the agent,
|
|
201
|
+
and broadcasting events. Used by both sync and async endpoints.
|
|
202
|
+
"""
|
|
244
203
|
session = await get_or_load_session(state, session_id)
|
|
245
204
|
if session is None:
|
|
246
205
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
@@ -280,7 +239,8 @@ async def send_message( # noqa: PLR0915
|
|
|
280
239
|
for part in user_parts:
|
|
281
240
|
await state.broadcast_event(PartUpdatedEvent.create(part))
|
|
282
241
|
state.session_status[session_id] = SessionStatus(type="busy")
|
|
283
|
-
|
|
242
|
+
status_event = SessionStatusEvent.create(session_id, SessionStatus(type="busy"))
|
|
243
|
+
await state.broadcast_event(status_event)
|
|
284
244
|
# Extract user prompt text
|
|
285
245
|
user_prompt = extract_user_prompt_from_parts([p.model_dump() for p in request.parts])
|
|
286
246
|
# Create assistant message with sortable ID (must come after user message)
|
|
@@ -331,7 +291,7 @@ async def send_message( # noqa: PLR0915
|
|
|
331
291
|
agent = state.agent.agent_pool.all_agents.get(request.agent, state.agent)
|
|
332
292
|
|
|
333
293
|
# Stream events from the agent
|
|
334
|
-
async for event in agent.run_stream(user_prompt):
|
|
294
|
+
async for event in agent.run_stream(user_prompt, conversation_id=session_id):
|
|
335
295
|
match event:
|
|
336
296
|
# Text streaming start
|
|
337
297
|
case PartStartEvent(part=PydanticTextPart(content=delta)):
|
|
@@ -460,6 +420,8 @@ async def send_message( # noqa: PLR0915
|
|
|
460
420
|
tool_input=event_tool_input,
|
|
461
421
|
) if tool_call_id:
|
|
462
422
|
# Extract text content from items and accumulate
|
|
423
|
+
# TODO: Handle TerminalContentItem for bash tool streaming - need to
|
|
424
|
+
# properly stream terminal output to OpenCode UI metadata
|
|
463
425
|
new_output = ""
|
|
464
426
|
file_paths: list[str] = []
|
|
465
427
|
for item in items:
|
|
@@ -536,6 +498,7 @@ async def send_message( # noqa: PLR0915
|
|
|
536
498
|
case ToolCallCompleteEvent(
|
|
537
499
|
tool_call_id=tool_call_id,
|
|
538
500
|
tool_result=result,
|
|
501
|
+
metadata=event_metadata,
|
|
539
502
|
) if tool_call_id in tool_parts:
|
|
540
503
|
existing = tool_parts[tool_call_id]
|
|
541
504
|
result_str = str(result) if result else ""
|
|
@@ -555,6 +518,7 @@ async def send_message( # noqa: PLR0915
|
|
|
555
518
|
title=f"Completed {existing.tool}",
|
|
556
519
|
input=tool_input,
|
|
557
520
|
output=result_str,
|
|
521
|
+
metadata=event_metadata or {},
|
|
558
522
|
time=TimeStartEndCompacted(start=now, end=now_ms()),
|
|
559
523
|
)
|
|
560
524
|
|
|
@@ -582,6 +546,70 @@ async def send_message( # noqa: PLR0915
|
|
|
582
546
|
# Cost is in Decimal dollars, OpenCode expects float dollars
|
|
583
547
|
total_cost = float(msg.cost_info.total_cost)
|
|
584
548
|
|
|
549
|
+
# Sub-agent/team event - show final results only
|
|
550
|
+
case SubAgentEvent(
|
|
551
|
+
source_name=source_name,
|
|
552
|
+
source_type=source_type,
|
|
553
|
+
event=wrapped_event,
|
|
554
|
+
depth=depth,
|
|
555
|
+
):
|
|
556
|
+
indent = " " * (depth - 1)
|
|
557
|
+
|
|
558
|
+
match wrapped_event:
|
|
559
|
+
# Final message from sub-agent/team
|
|
560
|
+
case StreamCompleteEvent(message=msg):
|
|
561
|
+
# Show indicator
|
|
562
|
+
icon = "⚡" if source_type == "team_parallel" else "→"
|
|
563
|
+
type_label = (
|
|
564
|
+
" (parallel)"
|
|
565
|
+
if source_type == "team_parallel"
|
|
566
|
+
else " (sequential)"
|
|
567
|
+
if source_type == "team_sequential"
|
|
568
|
+
else ""
|
|
569
|
+
)
|
|
570
|
+
indicator = f"{indent}{icon} {source_name}{type_label}"
|
|
571
|
+
|
|
572
|
+
indicator_part = TextPart(
|
|
573
|
+
id=identifier.ascending("part"),
|
|
574
|
+
message_id=assistant_msg_id,
|
|
575
|
+
session_id=session_id,
|
|
576
|
+
text=indicator,
|
|
577
|
+
time=TimeStartEndOptional(start=now_ms()),
|
|
578
|
+
)
|
|
579
|
+
assistant_msg_with_parts.parts.append(indicator_part)
|
|
580
|
+
await state.broadcast_event(PartUpdatedEvent.create(indicator_part))
|
|
581
|
+
|
|
582
|
+
# Show complete message content
|
|
583
|
+
content = str(msg.content) if msg.content else "(no output)"
|
|
584
|
+
content_part = TextPart(
|
|
585
|
+
id=identifier.ascending("part"),
|
|
586
|
+
message_id=assistant_msg_id,
|
|
587
|
+
session_id=session_id,
|
|
588
|
+
text=content,
|
|
589
|
+
time=TimeStartEndOptional(start=now_ms()),
|
|
590
|
+
)
|
|
591
|
+
assistant_msg_with_parts.parts.append(content_part)
|
|
592
|
+
await state.broadcast_event(PartUpdatedEvent.create(content_part))
|
|
593
|
+
|
|
594
|
+
# Tool call completed - show one-line summary
|
|
595
|
+
case ToolCallCompleteEvent(tool_name=tool_name, tool_result=result):
|
|
596
|
+
# Preview result (first 60 chars)
|
|
597
|
+
result_str = str(result) if result else ""
|
|
598
|
+
preview = (
|
|
599
|
+
result_str[:60] + "..." if len(result_str) > 60 else result_str # noqa: PLR2004
|
|
600
|
+
)
|
|
601
|
+
summary = f"{indent} ├─ {tool_name}: {preview}"
|
|
602
|
+
|
|
603
|
+
summary_part = TextPart(
|
|
604
|
+
id=identifier.ascending("part"),
|
|
605
|
+
message_id=assistant_msg_id,
|
|
606
|
+
session_id=session_id,
|
|
607
|
+
text=summary,
|
|
608
|
+
time=TimeStartEndOptional(start=now_ms()),
|
|
609
|
+
)
|
|
610
|
+
assistant_msg_with_parts.parts.append(summary_part)
|
|
611
|
+
await state.broadcast_event(PartUpdatedEvent.create(summary_part))
|
|
612
|
+
|
|
585
613
|
# Compaction event - emit session.compacted SSE event
|
|
586
614
|
case CompactionEvent(session_id=compact_session_id, phase=phase):
|
|
587
615
|
if phase == "completed":
|
|
@@ -676,17 +704,45 @@ async def send_message( # noqa: PLR0915
|
|
|
676
704
|
state.sessions[session_id] = session.model_copy(
|
|
677
705
|
update={"time": TimeCreatedUpdated(created=session.time.created, updated=response_time)}
|
|
678
706
|
)
|
|
679
|
-
#
|
|
680
|
-
|
|
681
|
-
# Convert user_prompt to string if it's a list
|
|
682
|
-
prompt_str = user_prompt if isinstance(user_prompt, str) else str(user_prompt)
|
|
683
|
-
state.create_background_task(
|
|
684
|
-
_generate_session_title(state, session_id, prompt_str, response_text),
|
|
685
|
-
name=f"generate_title_{session_id}",
|
|
686
|
-
)
|
|
707
|
+
# Title generation now handled by StorageManager signal (on_title_generated in server.py)
|
|
708
|
+
# Agent calls log_conversation() → _generate_title_from_prompt() → emits title_generated signal
|
|
687
709
|
return assistant_msg_with_parts
|
|
688
710
|
|
|
689
711
|
|
|
712
|
+
@router.post("/message")
|
|
713
|
+
async def send_message(
|
|
714
|
+
session_id: str,
|
|
715
|
+
request: MessageRequest,
|
|
716
|
+
state: StateDep,
|
|
717
|
+
) -> MessageWithParts:
|
|
718
|
+
"""Send a message and wait for the agent's response.
|
|
719
|
+
|
|
720
|
+
This is the synchronous version - waits for completion before returning.
|
|
721
|
+
For async processing, use POST /session/{id}/prompt_async instead.
|
|
722
|
+
"""
|
|
723
|
+
return await _process_message(session_id, request, state)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
@router.post("/prompt_async", status_code=status.HTTP_204_NO_CONTENT)
|
|
727
|
+
async def send_message_async(
|
|
728
|
+
session_id: str,
|
|
729
|
+
request: MessageRequest,
|
|
730
|
+
state: StateDep,
|
|
731
|
+
) -> None:
|
|
732
|
+
"""Send a message asynchronously without waiting for response.
|
|
733
|
+
|
|
734
|
+
Starts the agent processing in the background and returns immediately.
|
|
735
|
+
Client should listen to SSE events to get updates.
|
|
736
|
+
|
|
737
|
+
Returns 204 No Content immediately.
|
|
738
|
+
"""
|
|
739
|
+
# Create background task to process the message
|
|
740
|
+
state.create_background_task(
|
|
741
|
+
_process_message(session_id, request, state),
|
|
742
|
+
name=f"process_message_{session_id}",
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
|
|
690
746
|
@router.get("/message/{message_id}")
|
|
691
747
|
async def get_message(
|
|
692
748
|
session_id: str,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Permission routes for OpenCode TUI compatibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException
|
|
6
|
+
|
|
7
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
8
|
+
from agentpool_server.opencode_server.models.events import PermissionResolvedEvent
|
|
9
|
+
from agentpool_server.opencode_server.routes.session_routes import PermissionResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
router = APIRouter(prefix="/permission", tags=["permission"])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.post("/{permission_id}/reply")
|
|
16
|
+
async def reply_to_permission(
|
|
17
|
+
permission_id: str,
|
|
18
|
+
body: PermissionResponse,
|
|
19
|
+
state: StateDep,
|
|
20
|
+
) -> bool:
|
|
21
|
+
"""Respond to a pending permission request (OpenCode TUI compatibility).
|
|
22
|
+
|
|
23
|
+
This endpoint handles the OpenCode TUI's expected format:
|
|
24
|
+
POST /permission/{permission_id}/reply
|
|
25
|
+
|
|
26
|
+
The response can be:
|
|
27
|
+
- "once": Allow this tool execution once
|
|
28
|
+
- "always": Always allow this tool (remembered for session)
|
|
29
|
+
- "reject": Reject this tool execution
|
|
30
|
+
"""
|
|
31
|
+
print(f"DEBUG permission endpoint: received reply '{body.reply}' for perm_id={permission_id}")
|
|
32
|
+
print(f"DEBUG permission endpoint: searching in {len(state.input_providers)} sessions")
|
|
33
|
+
# Find which session has this permission request
|
|
34
|
+
for session_id, input_provider in state.input_providers.items():
|
|
35
|
+
pending_perms = list(input_provider._pending_permissions.keys())
|
|
36
|
+
print(
|
|
37
|
+
f"DEBUG permission endpoint: session {session_id} has "
|
|
38
|
+
f"{len(pending_perms)} pending: {pending_perms}"
|
|
39
|
+
)
|
|
40
|
+
# Check if this permission belongs to this session
|
|
41
|
+
if permission_id in input_provider._pending_permissions:
|
|
42
|
+
print(f"DEBUG permission endpoint: found permission in session {session_id}")
|
|
43
|
+
# Resolve the permission
|
|
44
|
+
resolved = input_provider.resolve_permission(permission_id, body.reply)
|
|
45
|
+
print(f"DEBUG permission endpoint: resolve_permission returned {resolved}")
|
|
46
|
+
if not resolved:
|
|
47
|
+
raise HTTPException(
|
|
48
|
+
status_code=404,
|
|
49
|
+
detail="Permission not found or already resolved",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
await state.broadcast_event(
|
|
53
|
+
PermissionResolvedEvent.create(
|
|
54
|
+
session_id=session_id,
|
|
55
|
+
request_id=permission_id,
|
|
56
|
+
reply=body.reply,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
# Permission not found in any session
|
|
63
|
+
raise HTTPException(status_code=404, detail="Permission not found")
|
|
@@ -10,17 +10,15 @@ import contextlib
|
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from typing import TYPE_CHECKING, Any
|
|
12
12
|
|
|
13
|
-
from fastapi import APIRouter, HTTPException, WebSocketDisconnect
|
|
13
|
+
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect # noqa: TC002
|
|
14
14
|
|
|
15
|
-
from agentpool_server.opencode_server.
|
|
15
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
16
|
+
from agentpool_server.opencode_server.models import PtyCreateRequest, PtyInfo, PtyUpdateRequest
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
if TYPE_CHECKING:
|
|
19
20
|
from exxec.pty_manager import PtyManagerProtocol
|
|
20
|
-
from fastapi import WebSocket
|
|
21
21
|
|
|
22
|
-
from agentpool_server.opencode_server.dependencies import StateDep
|
|
23
|
-
from agentpool_server.opencode_server.models import PtyCreateRequest, PtyUpdateRequest
|
|
24
22
|
from agentpool_server.opencode_server.state import ServerState
|
|
25
23
|
|
|
26
24
|
|
|
@@ -96,10 +94,15 @@ async def create_pty(request: PtyCreateRequest, state: StateDep) -> PtyInfo:
|
|
|
96
94
|
from agentpool_server.opencode_server.models.events import PtyCreatedEvent
|
|
97
95
|
|
|
98
96
|
manager = _get_pty_manager(state)
|
|
97
|
+
# Limit number of PTY sessions to prevent resource exhaustion
|
|
98
|
+
sessions = await manager.list_sessions()
|
|
99
|
+
if len(sessions) >= 20: # Max 20 concurrent PTY sessions # noqa: PLR2004
|
|
100
|
+
detail = f"Too many PTY sessions ({len(sessions)}). Close some terminals first."
|
|
101
|
+
raise HTTPException(status_code=429, detail=detail)
|
|
99
102
|
|
|
100
103
|
# Use working dir from state if not specified
|
|
101
104
|
cwd = request.cwd or state.working_dir
|
|
102
|
-
|
|
105
|
+
print(f"Creating PTY: command={request.command}, args={request.args}, cwd={cwd}")
|
|
103
106
|
try:
|
|
104
107
|
info = await manager.create(
|
|
105
108
|
command=request.command,
|
|
@@ -107,25 +110,22 @@ async def create_pty(request: PtyCreateRequest, state: StateDep) -> PtyInfo:
|
|
|
107
110
|
cwd=cwd,
|
|
108
111
|
env=request.env,
|
|
109
112
|
)
|
|
113
|
+
print(f"PTY created successfully: {info.id}, status={info.status}")
|
|
110
114
|
except Exception as e:
|
|
111
115
|
raise HTTPException(status_code=400, detail=f"Failed to create PTY: {e}") from e
|
|
112
116
|
|
|
113
117
|
pty_id = info.id
|
|
114
118
|
title = request.title or f"Terminal {pty_id[-4:]}"
|
|
115
|
-
|
|
116
119
|
# Create session tracker for WebSocket subscribers
|
|
117
120
|
session = PtySession(pty_id=pty_id)
|
|
118
121
|
_pty_sessions[pty_id] = session
|
|
119
|
-
|
|
122
|
+
print(f"PTY session registered: {pty_id}, total sessions: {len(_pty_sessions)}")
|
|
120
123
|
# Start background task to read output and distribute to subscribers
|
|
121
124
|
session.read_task = asyncio.create_task(_read_pty_output(manager, pty_id, state))
|
|
122
|
-
|
|
123
125
|
pty_info = _convert_pty_info(info, title=title)
|
|
124
|
-
|
|
125
126
|
# Broadcast PTY created event
|
|
126
127
|
event = PtyCreatedEvent.create(info=pty_info.model_dump(by_alias=True))
|
|
127
128
|
await state.broadcast_event(event)
|
|
128
|
-
|
|
129
129
|
return pty_info
|
|
130
130
|
|
|
131
131
|
|
|
@@ -178,6 +178,7 @@ async def get_pty(pty_id: str, state: StateDep) -> PtyInfo:
|
|
|
178
178
|
return _convert_pty_info(info)
|
|
179
179
|
|
|
180
180
|
|
|
181
|
+
@router.put("/{pty_id}")
|
|
181
182
|
@router.patch("/{pty_id}")
|
|
182
183
|
async def update_pty(pty_id: str, request: PtyUpdateRequest, state: StateDep) -> PtyInfo:
|
|
183
184
|
"""Update PTY session (title, resize)."""
|
|
@@ -200,13 +201,10 @@ async def update_pty(pty_id: str, request: PtyUpdateRequest, state: StateDep) ->
|
|
|
200
201
|
|
|
201
202
|
# Title is handled at the API level, not in the PTY manager
|
|
202
203
|
title = request.title if request.title else f"Terminal {pty_id[-4:]}"
|
|
203
|
-
|
|
204
204
|
pty_info = _convert_pty_info(info, title=title)
|
|
205
|
-
|
|
206
205
|
# Broadcast PTY updated event
|
|
207
206
|
event = PtyUpdatedEvent.create(info=pty_info.model_dump(by_alias=True))
|
|
208
207
|
await state.broadcast_event(event)
|
|
209
|
-
|
|
210
208
|
return pty_info
|
|
211
209
|
|
|
212
210
|
|
|
@@ -216,12 +214,10 @@ async def remove_pty(pty_id: str, state: StateDep) -> dict[str, bool]:
|
|
|
216
214
|
from agentpool_server.opencode_server.models.events import PtyDeletedEvent
|
|
217
215
|
|
|
218
216
|
manager = _get_pty_manager(state)
|
|
219
|
-
|
|
220
217
|
# Kill the PTY session
|
|
221
218
|
success = await manager.kill(pty_id)
|
|
222
219
|
if not success:
|
|
223
220
|
raise HTTPException(status_code=404, detail="PTY session not found")
|
|
224
|
-
|
|
225
221
|
# Cleanup session tracker
|
|
226
222
|
session = _pty_sessions.pop(pty_id, None)
|
|
227
223
|
if session:
|
|
@@ -249,26 +245,32 @@ async def connect_pty(websocket: WebSocket, pty_id: str) -> None:
|
|
|
249
245
|
# Get state from websocket's app
|
|
250
246
|
|
|
251
247
|
state: ServerState = websocket.app.state.server_state
|
|
252
|
-
|
|
253
248
|
try:
|
|
254
249
|
manager = _get_pty_manager(state)
|
|
255
250
|
except HTTPException:
|
|
256
|
-
|
|
251
|
+
# Must accept before we can close
|
|
252
|
+
await websocket.accept()
|
|
253
|
+
await websocket.close(code=1003, reason="PTY not supported")
|
|
254
|
+
return
|
|
255
|
+
except Exception as e: # noqa: BLE001
|
|
256
|
+
await websocket.accept()
|
|
257
|
+
await websocket.close(code=1011, reason=f"Error: {e}")
|
|
257
258
|
return
|
|
258
259
|
|
|
260
|
+
# Check if PTY exists - if not, immediately reject like OpenCode does
|
|
259
261
|
info = await manager.get_info(pty_id)
|
|
260
262
|
if not info:
|
|
261
|
-
await websocket.
|
|
263
|
+
await websocket.accept()
|
|
264
|
+
await websocket.close(code=1003, reason="PTY session not found")
|
|
262
265
|
return
|
|
263
266
|
|
|
267
|
+
# PTY exists, accept the WebSocket connection
|
|
264
268
|
await websocket.accept()
|
|
265
|
-
|
|
266
269
|
# Get or create session tracker
|
|
267
270
|
if pty_id not in _pty_sessions:
|
|
268
271
|
_pty_sessions[pty_id] = PtySession(pty_id=pty_id)
|
|
269
272
|
session = _pty_sessions[pty_id]
|
|
270
273
|
session.subscribers.add(websocket)
|
|
271
|
-
|
|
272
274
|
# Send buffered output
|
|
273
275
|
if session.buffer:
|
|
274
276
|
try:
|
|
@@ -281,7 +283,6 @@ async def connect_pty(websocket: WebSocket, pty_id: str) -> None:
|
|
|
281
283
|
while True:
|
|
282
284
|
# Receive input from client
|
|
283
285
|
data = await websocket.receive_text()
|
|
284
|
-
|
|
285
286
|
# Write to PTY stdin
|
|
286
287
|
info = await manager.get_info(pty_id)
|
|
287
288
|
if info and info.status == "running":
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Question routes for OpenCode compatibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException
|
|
6
|
+
|
|
7
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
8
|
+
from agentpool_server.opencode_server.input_provider import OpenCodeInputProvider
|
|
9
|
+
from agentpool_server.opencode_server.models.events import (
|
|
10
|
+
QuestionRejectedEvent,
|
|
11
|
+
QuestionRepliedEvent,
|
|
12
|
+
)
|
|
13
|
+
from agentpool_server.opencode_server.models.question import QuestionReply, QuestionRequest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/question", tags=["question"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.get("/", response_model=list[QuestionRequest])
|
|
20
|
+
async def list_questions(state: StateDep) -> list[QuestionRequest]:
|
|
21
|
+
"""List all pending question requests.
|
|
22
|
+
|
|
23
|
+
Returns a list of all pending questions awaiting user response.
|
|
24
|
+
"""
|
|
25
|
+
questions = []
|
|
26
|
+
for question_id, pending in state.pending_questions.items():
|
|
27
|
+
questions.append(
|
|
28
|
+
QuestionRequest(
|
|
29
|
+
id=question_id,
|
|
30
|
+
session_id=pending.session_id,
|
|
31
|
+
questions=pending.questions,
|
|
32
|
+
tool=pending.tool,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
return questions
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.post("/{requestID}/reply")
|
|
39
|
+
async def reply_to_question(
|
|
40
|
+
requestID: str, # noqa: N803
|
|
41
|
+
reply: QuestionReply,
|
|
42
|
+
state: StateDep,
|
|
43
|
+
) -> bool:
|
|
44
|
+
"""Reply to a question request.
|
|
45
|
+
|
|
46
|
+
The user provides answers to the questions. Answers must be provided
|
|
47
|
+
as an array of arrays, where each inner array contains the selected
|
|
48
|
+
label(s) for that question.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
requestID: The question request ID
|
|
52
|
+
reply: The user's answers
|
|
53
|
+
state: Server state
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if the question was resolved successfully
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
HTTPException: If question not found or invalid provider
|
|
60
|
+
"""
|
|
61
|
+
pending = state.pending_questions.get(requestID)
|
|
62
|
+
if not pending:
|
|
63
|
+
raise HTTPException(status_code=404, detail="Question request not found")
|
|
64
|
+
|
|
65
|
+
session_id = pending.session_id
|
|
66
|
+
provider = state.input_providers.get(session_id)
|
|
67
|
+
|
|
68
|
+
if not isinstance(provider, OpenCodeInputProvider):
|
|
69
|
+
raise HTTPException(status_code=500, detail="Invalid provider for session")
|
|
70
|
+
|
|
71
|
+
# Resolve via provider
|
|
72
|
+
success = provider.resolve_question(requestID, reply.answers)
|
|
73
|
+
|
|
74
|
+
if not success:
|
|
75
|
+
raise HTTPException(status_code=404, detail="Question already resolved")
|
|
76
|
+
|
|
77
|
+
# Broadcast replied event
|
|
78
|
+
event = QuestionRepliedEvent.create(
|
|
79
|
+
session_id=session_id,
|
|
80
|
+
request_id=requestID,
|
|
81
|
+
answers=reply.answers,
|
|
82
|
+
)
|
|
83
|
+
await state.broadcast_event(event)
|
|
84
|
+
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@router.post("/{requestID}/reject")
|
|
89
|
+
async def reject_question(
|
|
90
|
+
requestID: str, # noqa: N803
|
|
91
|
+
state: StateDep,
|
|
92
|
+
) -> bool:
|
|
93
|
+
"""Reject a question request.
|
|
94
|
+
|
|
95
|
+
Called when the user dismisses the question without providing an answer.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
requestID: The question request ID
|
|
99
|
+
state: Server state
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if the question was rejected successfully
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
HTTPException: If question not found
|
|
106
|
+
"""
|
|
107
|
+
pending = state.pending_questions.get(requestID)
|
|
108
|
+
if not pending:
|
|
109
|
+
raise HTTPException(status_code=404, detail="Question request not found")
|
|
110
|
+
|
|
111
|
+
session_id = pending.session_id
|
|
112
|
+
future = pending.future
|
|
113
|
+
|
|
114
|
+
# Cancel the future
|
|
115
|
+
if not future.done():
|
|
116
|
+
future.cancel()
|
|
117
|
+
|
|
118
|
+
# Remove from pending
|
|
119
|
+
del state.pending_questions[requestID]
|
|
120
|
+
|
|
121
|
+
# Broadcast rejected event
|
|
122
|
+
event = QuestionRejectedEvent.create(
|
|
123
|
+
session_id=session_id,
|
|
124
|
+
request_id=requestID,
|
|
125
|
+
)
|
|
126
|
+
await state.broadcast_event(event)
|
|
127
|
+
|
|
128
|
+
return True
|