agentpool 2.1.9__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 +13 -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/bridge/README.md +15 -2
- acp/bridge/__init__.py +3 -2
- acp/bridge/__main__.py +60 -19
- acp/bridge/ws_server.py +173 -0
- acp/bridge/ws_server_cli.py +89 -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 +20 -50
- 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/stdio.py +39 -9
- acp/task/supervisor.py +2 -2
- acp/transports.py +362 -2
- acp/utils.py +17 -4
- agentpool/__init__.py +6 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +407 -277
- agentpool/agents/acp_agent/acp_converters.py +196 -38
- agentpool/agents/acp_agent/client_handler.py +191 -26
- agentpool/agents/acp_agent/session_state.py +17 -6
- agentpool/agents/agent.py +607 -572
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +176 -110
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +632 -17
- 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 +1058 -291
- agentpool/agents/claude_code_agent/converters.py +74 -143
- agentpool/agents/claude_code_agent/history.py +474 -0
- 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/context.py +40 -0
- agentpool/agents/events/__init__.py +24 -0
- agentpool/agents/events/builtin_handlers.py +67 -1
- agentpool/agents/events/event_emitter.py +32 -2
- agentpool/agents/events/events.py +104 -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 +67 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +56 -21
- agentpool/config_resources/__init__.py +38 -1
- 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 +10 -6
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +136 -289
- agentpool/delegation/team.py +58 -57
- agentpool/delegation/teamrun.py +51 -55
- agentpool/diagnostics/__init__.py +53 -0
- agentpool/diagnostics/lsp_manager.py +1593 -0
- agentpool/diagnostics/lsp_proxy.py +41 -0
- agentpool/diagnostics/lsp_proxy_script.py +229 -0
- agentpool/diagnostics/models.py +398 -0
- agentpool/functional/run.py +10 -4
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +76 -32
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +34 -54
- agentpool/mcp_server/registries/official_registry_client.py +35 -1
- agentpool/mcp_server/tool_bridge.py +186 -139
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- 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 +99 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +54 -35
- agentpool/messaging/processing.py +12 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -24
- agentpool/models/acp_agents/mcp_capable.py +126 -157
- agentpool/models/acp_agents/non_mcp.py +129 -95
- agentpool/models/agents.py +98 -76
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +144 -19
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +113 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +11 -1
- agentpool/resource_providers/aggregating.py +56 -5
- agentpool/resource_providers/base.py +70 -4
- 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 +89 -12
- agentpool/resource_providers/plan_provider.py +228 -46
- agentpool/resource_providers/pool.py +7 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +4 -2
- agentpool/sessions/__init__.py +4 -1
- agentpool/sessions/manager.py +33 -5
- agentpool/sessions/models.py +59 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +572 -49
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +538 -20
- 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/file_watcher.py +269 -0
- agentpool/utils/identifiers.py +121 -0
- agentpool/utils/pydantic_ai_helpers.py +46 -0
- agentpool/utils/streams.py +616 -2
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
- agentpool-2.5.0.dist-info/RECORD +579 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +24 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +100 -21
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +42 -5
- agentpool_commands/agents.py +75 -2
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- 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/tools.py +57 -0
- agentpool_commands/utils.py +80 -30
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -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 +132 -6
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +82 -38
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -22
- agentpool_config/toolsets.py +109 -233
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +234 -181
- agentpool_server/acp_server/commands/acp_commands.py +151 -156
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +24 -90
- agentpool_server/acp_server/session.py +173 -331
- 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/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +401 -0
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +19 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +975 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +421 -0
- agentpool_server/opencode_server/models/__init__.py +250 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +72 -0
- agentpool_server/opencode_server/models/base.py +26 -0
- agentpool_server/opencode_server/models/common.py +23 -0
- agentpool_server/opencode_server/models/config.py +37 -0
- agentpool_server/opencode_server/models/events.py +821 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +44 -0
- agentpool_server/opencode_server/models/message.py +179 -0
- agentpool_server/opencode_server/models/parts.py +323 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +111 -0
- agentpool_server/opencode_server/routes/__init__.py +29 -0
- agentpool_server/opencode_server/routes/agent_routes.py +473 -0
- agentpool_server/opencode_server/routes/app_routes.py +202 -0
- agentpool_server/opencode_server/routes/config_routes.py +302 -0
- agentpool_server/opencode_server/routes/file_routes.py +571 -0
- agentpool_server/opencode_server/routes/global_routes.py +94 -0
- agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
- agentpool_server/opencode_server/routes/message_routes.py +761 -0
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +300 -0
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +1276 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +475 -0
- agentpool_server/opencode_server/state.py +151 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +12 -0
- agentpool_storage/base.py +184 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/claude_provider/provider.py +1089 -0
- agentpool_storage/file_provider.py +278 -15
- agentpool_storage/memory_provider.py +193 -12
- agentpool_storage/models.py +3 -0
- 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/project_store.py +325 -0
- agentpool_storage/session_store.py +26 -6
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +269 -3
- agentpool_storage/sql_provider/utils.py +12 -13
- 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 -12
- agentpool_toolsets/builtin/code.py +96 -57
- agentpool_toolsets/builtin/debug.py +118 -48
- agentpool_toolsets/builtin/execution_environment.py +115 -230
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +9 -4
- 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/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +99 -7
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +511 -0
- agentpool_toolsets/mcp_run_toolset.py +87 -12
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool-2.1.9.dist-info/RECORD +0 -474
- 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/text_log_provider.py +0 -275
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
"""Message routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Query, status
|
|
8
|
+
from pydantic_ai import FunctionToolCallEvent
|
|
9
|
+
from pydantic_ai.messages import (
|
|
10
|
+
PartDeltaEvent,
|
|
11
|
+
PartStartEvent,
|
|
12
|
+
TextPart as PydanticTextPart,
|
|
13
|
+
TextPartDelta,
|
|
14
|
+
ToolCallPart as PydanticToolCallPart,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from agentpool.agents.events import (
|
|
18
|
+
CompactionEvent,
|
|
19
|
+
FileContentItem,
|
|
20
|
+
LocationContentItem,
|
|
21
|
+
StreamCompleteEvent,
|
|
22
|
+
SubAgentEvent,
|
|
23
|
+
TextContentItem,
|
|
24
|
+
ToolCallCompleteEvent,
|
|
25
|
+
ToolCallProgressEvent,
|
|
26
|
+
ToolCallStartEvent,
|
|
27
|
+
)
|
|
28
|
+
from agentpool.agents.events.infer_info import derive_rich_tool_info
|
|
29
|
+
from agentpool.utils import identifiers as identifier
|
|
30
|
+
from agentpool.utils.pydantic_ai_helpers import safe_args_as_dict
|
|
31
|
+
from agentpool_server.opencode_server.converters import (
|
|
32
|
+
_convert_params_for_ui,
|
|
33
|
+
extract_user_prompt_from_parts,
|
|
34
|
+
opencode_to_chat_message,
|
|
35
|
+
)
|
|
36
|
+
from agentpool_server.opencode_server.dependencies import StateDep
|
|
37
|
+
from agentpool_server.opencode_server.models import (
|
|
38
|
+
AssistantMessage,
|
|
39
|
+
MessagePath,
|
|
40
|
+
MessageRequest,
|
|
41
|
+
MessageTime,
|
|
42
|
+
MessageUpdatedEvent,
|
|
43
|
+
MessageWithParts,
|
|
44
|
+
PartUpdatedEvent,
|
|
45
|
+
SessionCompactedEvent,
|
|
46
|
+
SessionErrorEvent,
|
|
47
|
+
SessionIdleEvent,
|
|
48
|
+
SessionStatus,
|
|
49
|
+
SessionStatusEvent,
|
|
50
|
+
StepFinishPart,
|
|
51
|
+
StepStartPart,
|
|
52
|
+
TextPart,
|
|
53
|
+
TimeCreated,
|
|
54
|
+
TimeCreatedUpdated,
|
|
55
|
+
TimeStartEnd,
|
|
56
|
+
Tokens,
|
|
57
|
+
TokensCache,
|
|
58
|
+
ToolPart,
|
|
59
|
+
ToolStateCompleted,
|
|
60
|
+
ToolStateError,
|
|
61
|
+
ToolStateRunning,
|
|
62
|
+
UserMessage,
|
|
63
|
+
)
|
|
64
|
+
from agentpool_server.opencode_server.models.message import UserMessageModel
|
|
65
|
+
from agentpool_server.opencode_server.models.parts import (
|
|
66
|
+
StepFinishTokens,
|
|
67
|
+
TimeStart,
|
|
68
|
+
TimeStartEndCompacted,
|
|
69
|
+
TimeStartEndOptional,
|
|
70
|
+
TokenCache,
|
|
71
|
+
)
|
|
72
|
+
from agentpool_server.opencode_server.routes.session_routes import get_or_load_session
|
|
73
|
+
from agentpool_server.opencode_server.time_utils import now_ms
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if TYPE_CHECKING:
|
|
77
|
+
from agentpool_server.opencode_server.models import (
|
|
78
|
+
Part,
|
|
79
|
+
)
|
|
80
|
+
from agentpool_server.opencode_server.state import ServerState
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _warmup_lsp_for_files(state: ServerState, file_paths: list[str]) -> None:
|
|
84
|
+
"""Warm up LSP servers for the given file paths.
|
|
85
|
+
|
|
86
|
+
This starts LSP servers asynchronously based on file extensions.
|
|
87
|
+
Like OpenCode's LSP.touchFile(), this triggers server startup without waiting.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
state: Server state with LSP manager
|
|
91
|
+
file_paths: List of file paths that were accessed
|
|
92
|
+
"""
|
|
93
|
+
import logging
|
|
94
|
+
|
|
95
|
+
logging.getLogger(__name__)
|
|
96
|
+
print(f"[LSP] _warmup_lsp_for_files called with: {file_paths}")
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
lsp_manager = state.get_or_create_lsp_manager()
|
|
100
|
+
print("[LSP] Got LSP manager successfully")
|
|
101
|
+
except RuntimeError as e:
|
|
102
|
+
# No execution environment available for LSP
|
|
103
|
+
print(f"[LSP] No LSP manager: {e}")
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
async def warmup_files() -> None:
|
|
107
|
+
"""Start LSP servers for each file path."""
|
|
108
|
+
print("[LSP] warmup_files task started")
|
|
109
|
+
from agentpool_server.opencode_server.models.events import LspUpdatedEvent
|
|
110
|
+
|
|
111
|
+
servers_started = False
|
|
112
|
+
for path in file_paths:
|
|
113
|
+
# Find appropriate server for this file
|
|
114
|
+
server_info = lsp_manager.get_server_for_file(path)
|
|
115
|
+
print(f"[LSP] Server for {path}: {server_info.id if server_info else None}")
|
|
116
|
+
if server_info is None:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
server_id = server_info.id
|
|
120
|
+
if lsp_manager.is_running(server_id):
|
|
121
|
+
print(f"[LSP] Server {server_id} already running")
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Start server for workspace root
|
|
125
|
+
root_uri = f"file://{state.working_dir}"
|
|
126
|
+
try:
|
|
127
|
+
print(f"[LSP] Starting server {server_id}...")
|
|
128
|
+
await lsp_manager.start_server(server_id, root_uri)
|
|
129
|
+
servers_started = True
|
|
130
|
+
print(f"[LSP] Server {server_id} started successfully")
|
|
131
|
+
except Exception as e: # noqa: BLE001
|
|
132
|
+
# Don't fail on LSP startup errors
|
|
133
|
+
print(f"[LSP] Failed to start server {server_id}: {e}")
|
|
134
|
+
|
|
135
|
+
# Emit lsp.updated event if any servers started
|
|
136
|
+
if servers_started:
|
|
137
|
+
print("[LSP] Broadcasting LspUpdatedEvent")
|
|
138
|
+
await state.broadcast_event(LspUpdatedEvent.create())
|
|
139
|
+
print("[LSP] warmup_files task completed")
|
|
140
|
+
|
|
141
|
+
# Run warmup in background (don't block the event handler)
|
|
142
|
+
print("[LSP] Creating background task for warmup")
|
|
143
|
+
state.create_background_task(warmup_files(), name="lsp-warmup")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def persist_message_to_storage(
|
|
147
|
+
state: ServerState,
|
|
148
|
+
msg: MessageWithParts,
|
|
149
|
+
session_id: str,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Persist an OpenCode message to storage.
|
|
152
|
+
|
|
153
|
+
Converts the OpenCode MessageWithParts to ChatMessage and saves it.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
state: Server state with pool reference
|
|
157
|
+
msg: OpenCode message to persist
|
|
158
|
+
session_id: Session/conversation ID
|
|
159
|
+
"""
|
|
160
|
+
if state.pool.storage is None:
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
# Convert to ChatMessage
|
|
165
|
+
chat_msg = opencode_to_chat_message(msg, conversation_id=session_id)
|
|
166
|
+
# Persist via storage manager
|
|
167
|
+
await state.pool.storage.log_message(chat_msg)
|
|
168
|
+
except Exception: # noqa: BLE001
|
|
169
|
+
# Don't fail the request if storage fails
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
router = APIRouter(prefix="/session/{session_id}", tags=["message"])
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@router.get("/message")
|
|
177
|
+
async def list_messages(
|
|
178
|
+
session_id: str,
|
|
179
|
+
state: StateDep,
|
|
180
|
+
limit: int | None = Query(default=None),
|
|
181
|
+
) -> list[MessageWithParts]:
|
|
182
|
+
"""List messages in a session."""
|
|
183
|
+
session = await get_or_load_session(state, session_id)
|
|
184
|
+
if session is None:
|
|
185
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
186
|
+
|
|
187
|
+
messages = state.messages.get(session_id, [])
|
|
188
|
+
if limit:
|
|
189
|
+
messages = messages[-limit:]
|
|
190
|
+
return messages
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def _process_message( # noqa: PLR0915
|
|
194
|
+
session_id: str,
|
|
195
|
+
request: MessageRequest,
|
|
196
|
+
state: StateDep,
|
|
197
|
+
) -> MessageWithParts:
|
|
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
|
+
"""
|
|
203
|
+
session = await get_or_load_session(state, session_id)
|
|
204
|
+
if session is None:
|
|
205
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
206
|
+
|
|
207
|
+
now = now_ms()
|
|
208
|
+
# Create user message with sortable ID
|
|
209
|
+
user_msg_id = identifier.ascending("message", request.message_id)
|
|
210
|
+
user_message = UserMessage(
|
|
211
|
+
id=user_msg_id,
|
|
212
|
+
session_id=session_id,
|
|
213
|
+
time=TimeCreated(created=now),
|
|
214
|
+
agent=request.agent or "default",
|
|
215
|
+
model=UserMessageModel(
|
|
216
|
+
provider_id=request.model.provider_id if request.model else "agentpool",
|
|
217
|
+
model_id=request.model.model_id if request.model else "default",
|
|
218
|
+
),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Create parts from request
|
|
222
|
+
user_parts: list[Part] = [
|
|
223
|
+
TextPart(
|
|
224
|
+
id=identifier.ascending("part"),
|
|
225
|
+
message_id=user_msg_id,
|
|
226
|
+
session_id=session_id,
|
|
227
|
+
text=part.text,
|
|
228
|
+
)
|
|
229
|
+
for part in request.parts
|
|
230
|
+
if part.type == "text"
|
|
231
|
+
]
|
|
232
|
+
user_msg_with_parts = MessageWithParts(info=user_message, parts=user_parts)
|
|
233
|
+
state.messages[session_id].append(user_msg_with_parts)
|
|
234
|
+
# Persist user message to storage
|
|
235
|
+
await persist_message_to_storage(state, user_msg_with_parts, session_id)
|
|
236
|
+
# Broadcast user message created event
|
|
237
|
+
await state.broadcast_event(MessageUpdatedEvent.create(user_message))
|
|
238
|
+
# Broadcast user message parts so they appear in UI
|
|
239
|
+
for part in user_parts:
|
|
240
|
+
await state.broadcast_event(PartUpdatedEvent.create(part))
|
|
241
|
+
state.session_status[session_id] = SessionStatus(type="busy")
|
|
242
|
+
status_event = SessionStatusEvent.create(session_id, SessionStatus(type="busy"))
|
|
243
|
+
await state.broadcast_event(status_event)
|
|
244
|
+
# Extract user prompt text
|
|
245
|
+
user_prompt = extract_user_prompt_from_parts([p.model_dump() for p in request.parts])
|
|
246
|
+
# Create assistant message with sortable ID (must come after user message)
|
|
247
|
+
assistant_msg_id = identifier.ascending("message")
|
|
248
|
+
tokens = Tokens(cache=TokensCache(read=0, write=0))
|
|
249
|
+
assistant_message = AssistantMessage(
|
|
250
|
+
id=assistant_msg_id,
|
|
251
|
+
session_id=session_id,
|
|
252
|
+
parent_id=user_msg_id, # Link to user message
|
|
253
|
+
model_id=request.model.model_id if request.model else "default",
|
|
254
|
+
provider_id=request.model.provider_id if request.model else "agentpool",
|
|
255
|
+
mode=request.agent or "default",
|
|
256
|
+
agent=request.agent or "default",
|
|
257
|
+
path=MessagePath(cwd=state.working_dir, root=state.working_dir),
|
|
258
|
+
time=MessageTime(created=now, completed=None),
|
|
259
|
+
tokens=tokens,
|
|
260
|
+
cost=0.0,
|
|
261
|
+
)
|
|
262
|
+
# Initialize assistant message with empty parts
|
|
263
|
+
assistant_msg_with_parts = MessageWithParts(info=assistant_message, parts=[])
|
|
264
|
+
state.messages[session_id].append(assistant_msg_with_parts)
|
|
265
|
+
# Broadcast assistant message created
|
|
266
|
+
await state.broadcast_event(MessageUpdatedEvent.create(assistant_message))
|
|
267
|
+
# Add step-start part
|
|
268
|
+
step_start = StepStartPart(
|
|
269
|
+
id=identifier.ascending("part"),
|
|
270
|
+
message_id=assistant_msg_id,
|
|
271
|
+
session_id=session_id,
|
|
272
|
+
)
|
|
273
|
+
assistant_msg_with_parts.parts.append(step_start)
|
|
274
|
+
await state.broadcast_event(PartUpdatedEvent.create(step_start))
|
|
275
|
+
# Call the agent
|
|
276
|
+
response_text = ""
|
|
277
|
+
input_tokens = 0
|
|
278
|
+
output_tokens = 0
|
|
279
|
+
total_cost = 0.0 # Cost in dollars
|
|
280
|
+
tool_parts: dict[str, ToolPart] = {} # Track tool parts by call_id
|
|
281
|
+
tool_outputs: dict[str, str] = {} # Track accumulated output per tool call
|
|
282
|
+
tool_inputs: dict[str, dict[str, Any]] = {} # Track inputs per tool call
|
|
283
|
+
# Track streaming text part for incremental updates
|
|
284
|
+
text_part: TextPart | None = None
|
|
285
|
+
text_part_id: str | None = None
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
# Get the specified agent from the pool, or fall back to default
|
|
289
|
+
agent = state.agent
|
|
290
|
+
if request.agent and state.agent.agent_pool is not None:
|
|
291
|
+
agent = state.agent.agent_pool.all_agents.get(request.agent, state.agent)
|
|
292
|
+
|
|
293
|
+
# Stream events from the agent
|
|
294
|
+
async for event in agent.run_stream(user_prompt, conversation_id=session_id):
|
|
295
|
+
match event:
|
|
296
|
+
# Text streaming start
|
|
297
|
+
case PartStartEvent(part=PydanticTextPart(content=delta)):
|
|
298
|
+
response_text = delta
|
|
299
|
+
text_part_id = identifier.ascending("part")
|
|
300
|
+
text_part = TextPart(
|
|
301
|
+
id=text_part_id,
|
|
302
|
+
message_id=assistant_msg_id,
|
|
303
|
+
session_id=session_id,
|
|
304
|
+
text=delta,
|
|
305
|
+
)
|
|
306
|
+
assistant_msg_with_parts.parts.append(text_part)
|
|
307
|
+
await state.broadcast_event(PartUpdatedEvent.create(text_part, delta=delta))
|
|
308
|
+
|
|
309
|
+
# Text streaming delta
|
|
310
|
+
case PartDeltaEvent(delta=TextPartDelta(content_delta=delta)) if delta:
|
|
311
|
+
response_text += delta
|
|
312
|
+
if text_part is not None:
|
|
313
|
+
text_part = TextPart(
|
|
314
|
+
id=text_part.id,
|
|
315
|
+
message_id=assistant_msg_id,
|
|
316
|
+
session_id=session_id,
|
|
317
|
+
text=response_text,
|
|
318
|
+
)
|
|
319
|
+
# Update in parts list
|
|
320
|
+
for i, p in enumerate(assistant_msg_with_parts.parts):
|
|
321
|
+
if isinstance(p, TextPart) and p.id == text_part.id:
|
|
322
|
+
assistant_msg_with_parts.parts[i] = text_part
|
|
323
|
+
break
|
|
324
|
+
await state.broadcast_event(PartUpdatedEvent.create(text_part, delta=delta))
|
|
325
|
+
|
|
326
|
+
# Tool call start - from Claude Code agent or toolsets
|
|
327
|
+
case ToolCallStartEvent(
|
|
328
|
+
tool_name=tool_name,
|
|
329
|
+
tool_call_id=tool_call_id,
|
|
330
|
+
raw_input=raw_input,
|
|
331
|
+
title=title,
|
|
332
|
+
):
|
|
333
|
+
# Convert param names for OpenCode TUI compatibility
|
|
334
|
+
ui_input = _convert_params_for_ui(raw_input) if raw_input else {}
|
|
335
|
+
if tool_call_id in tool_parts:
|
|
336
|
+
# Update existing part with the custom title
|
|
337
|
+
existing = tool_parts[tool_call_id]
|
|
338
|
+
tool_inputs[tool_call_id] = ui_input or tool_inputs.get(tool_call_id, {})
|
|
339
|
+
|
|
340
|
+
updated = ToolPart(
|
|
341
|
+
id=existing.id,
|
|
342
|
+
message_id=existing.message_id,
|
|
343
|
+
session_id=existing.session_id,
|
|
344
|
+
tool=existing.tool,
|
|
345
|
+
call_id=existing.call_id,
|
|
346
|
+
state=ToolStateRunning(
|
|
347
|
+
status="running",
|
|
348
|
+
time=TimeStart(start=now_ms()),
|
|
349
|
+
input=tool_inputs[tool_call_id],
|
|
350
|
+
title=title,
|
|
351
|
+
),
|
|
352
|
+
)
|
|
353
|
+
tool_parts[tool_call_id] = updated
|
|
354
|
+
for i, p in enumerate(assistant_msg_with_parts.parts):
|
|
355
|
+
if isinstance(p, ToolPart) and p.id == existing.id:
|
|
356
|
+
assistant_msg_with_parts.parts[i] = updated
|
|
357
|
+
break
|
|
358
|
+
await state.broadcast_event(PartUpdatedEvent.create(updated))
|
|
359
|
+
else:
|
|
360
|
+
# Create new tool part with the title
|
|
361
|
+
tool_inputs[tool_call_id] = ui_input
|
|
362
|
+
tool_outputs[tool_call_id] = ""
|
|
363
|
+
tool_state = ToolStateRunning(
|
|
364
|
+
status="running",
|
|
365
|
+
time=TimeStart(start=now_ms()),
|
|
366
|
+
input=ui_input,
|
|
367
|
+
title=title,
|
|
368
|
+
)
|
|
369
|
+
tool_part = ToolPart(
|
|
370
|
+
id=identifier.ascending("part"),
|
|
371
|
+
message_id=assistant_msg_id,
|
|
372
|
+
session_id=session_id,
|
|
373
|
+
tool=tool_name,
|
|
374
|
+
call_id=tool_call_id,
|
|
375
|
+
state=tool_state,
|
|
376
|
+
)
|
|
377
|
+
tool_parts[tool_call_id] = tool_part
|
|
378
|
+
assistant_msg_with_parts.parts.append(tool_part)
|
|
379
|
+
await state.broadcast_event(PartUpdatedEvent.create(tool_part))
|
|
380
|
+
|
|
381
|
+
# Pydantic-ai tool call events (fallback for pydantic-ai agents)
|
|
382
|
+
case (
|
|
383
|
+
FunctionToolCallEvent(part=tc_part)
|
|
384
|
+
| PartStartEvent(part=PydanticToolCallPart() as tc_part)
|
|
385
|
+
) if tc_part.tool_call_id not in tool_parts:
|
|
386
|
+
tool_call_id = tc_part.tool_call_id
|
|
387
|
+
tool_name = tc_part.tool_name
|
|
388
|
+
raw_input = safe_args_as_dict(tc_part)
|
|
389
|
+
# Convert param names for OpenCode TUI compatibility
|
|
390
|
+
ui_input = _convert_params_for_ui(raw_input)
|
|
391
|
+
# Store input and initialize output accumulator
|
|
392
|
+
tool_inputs[tool_call_id] = ui_input
|
|
393
|
+
tool_outputs[tool_call_id] = ""
|
|
394
|
+
# Derive initial title; toolset events may update it later
|
|
395
|
+
rich_info = derive_rich_tool_info(tool_name, raw_input)
|
|
396
|
+
tool_state = ToolStateRunning(
|
|
397
|
+
status="running",
|
|
398
|
+
time=TimeStart(start=now_ms()),
|
|
399
|
+
input=ui_input,
|
|
400
|
+
title=rich_info.title,
|
|
401
|
+
)
|
|
402
|
+
tool_part = ToolPart(
|
|
403
|
+
id=identifier.ascending("part"),
|
|
404
|
+
message_id=assistant_msg_id,
|
|
405
|
+
session_id=session_id,
|
|
406
|
+
tool=tool_name,
|
|
407
|
+
call_id=tool_call_id,
|
|
408
|
+
state=tool_state,
|
|
409
|
+
)
|
|
410
|
+
tool_parts[tool_call_id] = tool_part
|
|
411
|
+
assistant_msg_with_parts.parts.append(tool_part)
|
|
412
|
+
await state.broadcast_event(PartUpdatedEvent.create(tool_part))
|
|
413
|
+
|
|
414
|
+
# Tool call progress
|
|
415
|
+
case ToolCallProgressEvent(
|
|
416
|
+
tool_call_id=tool_call_id,
|
|
417
|
+
title=title,
|
|
418
|
+
items=items,
|
|
419
|
+
tool_name=tool_name,
|
|
420
|
+
tool_input=event_tool_input,
|
|
421
|
+
) if tool_call_id:
|
|
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
|
|
425
|
+
new_output = ""
|
|
426
|
+
file_paths: list[str] = []
|
|
427
|
+
for item in items:
|
|
428
|
+
if isinstance(item, TextContentItem):
|
|
429
|
+
new_output += item.text
|
|
430
|
+
elif isinstance(item, FileContentItem):
|
|
431
|
+
new_output += item.content
|
|
432
|
+
file_paths.append(item.path)
|
|
433
|
+
elif isinstance(item, LocationContentItem):
|
|
434
|
+
file_paths.append(item.path)
|
|
435
|
+
|
|
436
|
+
# Warm up LSP servers for accessed files (async, don't wait)
|
|
437
|
+
if file_paths:
|
|
438
|
+
_warmup_lsp_for_files(state, file_paths)
|
|
439
|
+
|
|
440
|
+
# Accumulate output (OpenCode streams via metadata.output)
|
|
441
|
+
if new_output:
|
|
442
|
+
tool_outputs[tool_call_id] = tool_outputs.get(tool_call_id, "") + new_output
|
|
443
|
+
|
|
444
|
+
if tool_call_id in tool_parts:
|
|
445
|
+
# Update existing part
|
|
446
|
+
existing = tool_parts[tool_call_id]
|
|
447
|
+
existing_title = getattr(existing.state, "title", "")
|
|
448
|
+
tool_input = tool_inputs.get(tool_call_id, {})
|
|
449
|
+
accumulated_output = tool_outputs.get(tool_call_id, "")
|
|
450
|
+
tool_state = ToolStateRunning(
|
|
451
|
+
status="running",
|
|
452
|
+
time=TimeStart(start=now_ms()),
|
|
453
|
+
title=title or existing_title,
|
|
454
|
+
input=tool_input,
|
|
455
|
+
metadata={"output": accumulated_output} if accumulated_output else None,
|
|
456
|
+
)
|
|
457
|
+
updated = ToolPart(
|
|
458
|
+
id=existing.id,
|
|
459
|
+
message_id=existing.message_id,
|
|
460
|
+
session_id=existing.session_id,
|
|
461
|
+
tool=existing.tool,
|
|
462
|
+
call_id=existing.call_id,
|
|
463
|
+
state=tool_state,
|
|
464
|
+
)
|
|
465
|
+
tool_parts[tool_call_id] = updated
|
|
466
|
+
for i, p in enumerate(assistant_msg_with_parts.parts):
|
|
467
|
+
if isinstance(p, ToolPart) and p.id == existing.id:
|
|
468
|
+
assistant_msg_with_parts.parts[i] = updated
|
|
469
|
+
break
|
|
470
|
+
await state.broadcast_event(PartUpdatedEvent.create(updated))
|
|
471
|
+
else:
|
|
472
|
+
# Create new tool part from progress event
|
|
473
|
+
ui_input = (
|
|
474
|
+
_convert_params_for_ui(event_tool_input) if event_tool_input else {}
|
|
475
|
+
)
|
|
476
|
+
tool_inputs[tool_call_id] = ui_input
|
|
477
|
+
accumulated_output = tool_outputs.get(tool_call_id, "")
|
|
478
|
+
tool_state = ToolStateRunning(
|
|
479
|
+
status="running",
|
|
480
|
+
time=TimeStart(start=now_ms()),
|
|
481
|
+
input=ui_input,
|
|
482
|
+
title=title or tool_name or "Running...",
|
|
483
|
+
metadata={"output": accumulated_output} if accumulated_output else None,
|
|
484
|
+
)
|
|
485
|
+
tool_part = ToolPart(
|
|
486
|
+
id=identifier.ascending("part"),
|
|
487
|
+
message_id=assistant_msg_id,
|
|
488
|
+
session_id=session_id,
|
|
489
|
+
tool=tool_name or "unknown",
|
|
490
|
+
call_id=tool_call_id,
|
|
491
|
+
state=tool_state,
|
|
492
|
+
)
|
|
493
|
+
tool_parts[tool_call_id] = tool_part
|
|
494
|
+
assistant_msg_with_parts.parts.append(tool_part)
|
|
495
|
+
await state.broadcast_event(PartUpdatedEvent.create(tool_part))
|
|
496
|
+
|
|
497
|
+
# Tool call complete
|
|
498
|
+
case ToolCallCompleteEvent(
|
|
499
|
+
tool_call_id=tool_call_id,
|
|
500
|
+
tool_result=result,
|
|
501
|
+
metadata=event_metadata,
|
|
502
|
+
) if tool_call_id in tool_parts:
|
|
503
|
+
existing = tool_parts[tool_call_id]
|
|
504
|
+
result_str = str(result) if result else ""
|
|
505
|
+
tool_input = tool_inputs.get(tool_call_id, {})
|
|
506
|
+
is_error = isinstance(result, dict) and result.get("error")
|
|
507
|
+
|
|
508
|
+
if is_error:
|
|
509
|
+
new_state: ToolStateCompleted | ToolStateError = ToolStateError(
|
|
510
|
+
status="error",
|
|
511
|
+
error=str(result.get("error", "Unknown error")),
|
|
512
|
+
input=tool_input,
|
|
513
|
+
time=TimeStartEnd(start=now, end=now_ms()),
|
|
514
|
+
)
|
|
515
|
+
else:
|
|
516
|
+
new_state = ToolStateCompleted(
|
|
517
|
+
status="completed",
|
|
518
|
+
title=f"Completed {existing.tool}",
|
|
519
|
+
input=tool_input,
|
|
520
|
+
output=result_str,
|
|
521
|
+
metadata=event_metadata or {},
|
|
522
|
+
time=TimeStartEndCompacted(start=now, end=now_ms()),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
updated = ToolPart(
|
|
526
|
+
id=existing.id,
|
|
527
|
+
message_id=existing.message_id,
|
|
528
|
+
session_id=existing.session_id,
|
|
529
|
+
tool=existing.tool,
|
|
530
|
+
call_id=existing.call_id,
|
|
531
|
+
state=new_state,
|
|
532
|
+
)
|
|
533
|
+
tool_parts[tool_call_id] = updated
|
|
534
|
+
for i, p in enumerate(assistant_msg_with_parts.parts):
|
|
535
|
+
if isinstance(p, ToolPart) and p.id == existing.id:
|
|
536
|
+
assistant_msg_with_parts.parts[i] = updated
|
|
537
|
+
break
|
|
538
|
+
await state.broadcast_event(PartUpdatedEvent.create(updated))
|
|
539
|
+
|
|
540
|
+
# Stream complete - extract token usage and cost
|
|
541
|
+
case StreamCompleteEvent(message=msg) if msg:
|
|
542
|
+
if msg.usage:
|
|
543
|
+
input_tokens = msg.usage.input_tokens or 0
|
|
544
|
+
output_tokens = msg.usage.output_tokens or 0
|
|
545
|
+
if msg.cost_info and msg.cost_info.total_cost:
|
|
546
|
+
# Cost is in Decimal dollars, OpenCode expects float dollars
|
|
547
|
+
total_cost = float(msg.cost_info.total_cost)
|
|
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
|
+
|
|
613
|
+
# Compaction event - emit session.compacted SSE event
|
|
614
|
+
case CompactionEvent(session_id=compact_session_id, phase=phase):
|
|
615
|
+
if phase == "completed":
|
|
616
|
+
await state.broadcast_event(
|
|
617
|
+
SessionCompactedEvent.create(session_id=compact_session_id)
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
except Exception as e: # noqa: BLE001
|
|
621
|
+
response_text = f"Error calling agent: {e}"
|
|
622
|
+
# Emit session error event
|
|
623
|
+
await state.broadcast_event(
|
|
624
|
+
SessionErrorEvent.create(
|
|
625
|
+
session_id=session_id,
|
|
626
|
+
error_name=type(e).__name__,
|
|
627
|
+
error_message=str(e),
|
|
628
|
+
)
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
response_time = now_ms()
|
|
632
|
+
|
|
633
|
+
# Create text part with response (only if we didn't stream it already)
|
|
634
|
+
if response_text and text_part is None:
|
|
635
|
+
text_part = TextPart(
|
|
636
|
+
id=identifier.ascending("part"),
|
|
637
|
+
message_id=assistant_msg_id,
|
|
638
|
+
session_id=session_id,
|
|
639
|
+
text=response_text,
|
|
640
|
+
time=TimeStartEndOptional(start=now, end=response_time),
|
|
641
|
+
)
|
|
642
|
+
assistant_msg_with_parts.parts.append(text_part)
|
|
643
|
+
|
|
644
|
+
# Broadcast text part update
|
|
645
|
+
await state.broadcast_event(PartUpdatedEvent.create(text_part))
|
|
646
|
+
elif text_part is not None:
|
|
647
|
+
# Update the streamed text part with final timing
|
|
648
|
+
final_text_part = TextPart(
|
|
649
|
+
id=text_part.id,
|
|
650
|
+
message_id=assistant_msg_id,
|
|
651
|
+
session_id=session_id,
|
|
652
|
+
text=response_text,
|
|
653
|
+
time=TimeStartEndOptional(start=now, end=response_time),
|
|
654
|
+
)
|
|
655
|
+
# Update in parts list
|
|
656
|
+
for i, p in enumerate(assistant_msg_with_parts.parts):
|
|
657
|
+
if isinstance(p, TextPart) and p.id == text_part.id:
|
|
658
|
+
assistant_msg_with_parts.parts[i] = final_text_part
|
|
659
|
+
break
|
|
660
|
+
|
|
661
|
+
step_finish = StepFinishPart(
|
|
662
|
+
id=identifier.ascending("part"),
|
|
663
|
+
message_id=assistant_msg_id,
|
|
664
|
+
session_id=session_id,
|
|
665
|
+
tokens=StepFinishTokens(
|
|
666
|
+
cache=TokenCache(read=0, write=0),
|
|
667
|
+
input=input_tokens,
|
|
668
|
+
output=output_tokens,
|
|
669
|
+
reasoning=0,
|
|
670
|
+
),
|
|
671
|
+
cost=total_cost,
|
|
672
|
+
)
|
|
673
|
+
assistant_msg_with_parts.parts.append(step_finish)
|
|
674
|
+
await state.broadcast_event(PartUpdatedEvent.create(step_finish))
|
|
675
|
+
|
|
676
|
+
print(f"Response text: {response_text[:100] if response_text else 'EMPTY'}...")
|
|
677
|
+
|
|
678
|
+
# Update assistant message with final timing and tokens
|
|
679
|
+
updated_assistant = assistant_message.model_copy(
|
|
680
|
+
update={
|
|
681
|
+
"time": MessageTime(created=now, completed=response_time),
|
|
682
|
+
"tokens": Tokens(
|
|
683
|
+
cache=TokensCache(read=0, write=0),
|
|
684
|
+
input=input_tokens,
|
|
685
|
+
output=output_tokens,
|
|
686
|
+
reasoning=0,
|
|
687
|
+
),
|
|
688
|
+
"cost": total_cost,
|
|
689
|
+
}
|
|
690
|
+
)
|
|
691
|
+
assistant_msg_with_parts.info = updated_assistant
|
|
692
|
+
|
|
693
|
+
# Broadcast final message update
|
|
694
|
+
await state.broadcast_event(MessageUpdatedEvent.create(updated_assistant))
|
|
695
|
+
# Persist assistant message to storage
|
|
696
|
+
await persist_message_to_storage(state, assistant_msg_with_parts, session_id)
|
|
697
|
+
# Mark session as not running
|
|
698
|
+
state.session_status[session_id] = SessionStatus(type="idle")
|
|
699
|
+
await state.broadcast_event(SessionStatusEvent.create(session_id, SessionStatus(type="idle")))
|
|
700
|
+
await state.broadcast_event(SessionIdleEvent.create(session_id))
|
|
701
|
+
|
|
702
|
+
# Update session timestamp
|
|
703
|
+
session = state.sessions[session_id]
|
|
704
|
+
state.sessions[session_id] = session.model_copy(
|
|
705
|
+
update={"time": TimeCreatedUpdated(created=session.time.created, updated=response_time)}
|
|
706
|
+
)
|
|
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
|
|
709
|
+
return assistant_msg_with_parts
|
|
710
|
+
|
|
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
|
+
|
|
746
|
+
@router.get("/message/{message_id}")
|
|
747
|
+
async def get_message(
|
|
748
|
+
session_id: str,
|
|
749
|
+
message_id: str,
|
|
750
|
+
state: StateDep,
|
|
751
|
+
) -> MessageWithParts:
|
|
752
|
+
"""Get a specific message."""
|
|
753
|
+
session = await get_or_load_session(state, session_id)
|
|
754
|
+
if session is None:
|
|
755
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
756
|
+
|
|
757
|
+
for msg in state.messages.get(session_id, []):
|
|
758
|
+
if msg.info.id == message_id:
|
|
759
|
+
return msg
|
|
760
|
+
|
|
761
|
+
raise HTTPException(status_code=404, detail="Message not found")
|