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
agentpool/agents/base_agent.py
CHANGED
|
@@ -4,34 +4,60 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from abc import abstractmethod
|
|
6
6
|
import asyncio
|
|
7
|
-
from
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from contextlib import suppress
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import TYPE_CHECKING, Any, overload
|
|
8
11
|
|
|
9
|
-
from anyenv import MultiEventHandler
|
|
10
|
-
from
|
|
12
|
+
from anyenv import MultiEventHandler, method_spawner
|
|
13
|
+
from anyenv.signals import BoundSignal, Signal
|
|
14
|
+
import anyio
|
|
11
15
|
|
|
12
|
-
from agentpool.agents.events import resolve_event_handlers
|
|
16
|
+
from agentpool.agents.events import StreamCompleteEvent, resolve_event_handlers
|
|
17
|
+
from agentpool.agents.modes import ModeInfo
|
|
13
18
|
from agentpool.log import get_logger
|
|
14
|
-
from agentpool.messaging import MessageHistory, MessageNode
|
|
19
|
+
from agentpool.messaging import ChatMessage, MessageHistory, MessageNode
|
|
20
|
+
from agentpool.prompts.convert import convert_prompts
|
|
15
21
|
from agentpool.tools.manager import ToolManager
|
|
22
|
+
from agentpool.utils.inspection import call_with_context
|
|
23
|
+
from agentpool.utils.now import get_now
|
|
16
24
|
|
|
17
25
|
|
|
18
26
|
if TYPE_CHECKING:
|
|
19
27
|
from collections.abc import AsyncIterator, Sequence
|
|
28
|
+
from datetime import datetime
|
|
20
29
|
|
|
21
|
-
from
|
|
30
|
+
from evented_config import EventConfig
|
|
22
31
|
from exxec import ExecutionEnvironment
|
|
32
|
+
from pydantic_ai import UserContent
|
|
33
|
+
from slashed import BaseCommand, CommandStore
|
|
34
|
+
from tokonomics.model_discovery.model_info import ModelInfo
|
|
23
35
|
|
|
36
|
+
from acp.schema import AvailableCommandsUpdate, ConfigOptionUpdate
|
|
37
|
+
from agentpool.agents.agent import Agent
|
|
24
38
|
from agentpool.agents.context import AgentContext
|
|
25
39
|
from agentpool.agents.events import RichAgentStreamEvent
|
|
26
|
-
from agentpool.
|
|
27
|
-
from agentpool.
|
|
40
|
+
from agentpool.agents.modes import ModeCategory, ModeInfo
|
|
41
|
+
from agentpool.common_types import (
|
|
42
|
+
AgentName,
|
|
43
|
+
BuiltinEventHandlerType,
|
|
44
|
+
IndividualEventHandler,
|
|
45
|
+
MCPServerStatus,
|
|
46
|
+
ProcessorCallback,
|
|
47
|
+
PromptCompatible,
|
|
48
|
+
)
|
|
49
|
+
from agentpool.delegation import AgentPool, Team, TeamRun
|
|
50
|
+
from agentpool.messaging import ChatMessage
|
|
51
|
+
from agentpool.talk.stats import MessageStats
|
|
28
52
|
from agentpool.ui.base import InputProvider
|
|
29
53
|
from agentpool_config.mcp_server import MCPServerConfig
|
|
54
|
+
from agentpool_config.nodes import ToolConfirmationMode
|
|
30
55
|
|
|
56
|
+
# Union type for state updates emitted via state_updated signal
|
|
57
|
+
type StateUpdate = ModeInfo | ModelInfo | AvailableCommandsUpdate | ConfigOptionUpdate
|
|
31
58
|
|
|
32
|
-
logger = get_logger(__name__)
|
|
33
59
|
|
|
34
|
-
|
|
60
|
+
logger = get_logger(__name__)
|
|
35
61
|
|
|
36
62
|
|
|
37
63
|
class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
@@ -46,8 +72,37 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
46
72
|
- _input_provider: Provider for user input/confirmations
|
|
47
73
|
- env: ExecutionEnvironment for running code/commands
|
|
48
74
|
- context property: Returns NodeContext for the agent
|
|
75
|
+
|
|
76
|
+
Signals:
|
|
77
|
+
- run_failed: Emitted when agent execution fails with error details
|
|
49
78
|
"""
|
|
50
79
|
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class RunFailedEvent:
|
|
82
|
+
"""Event emitted when agent execution fails."""
|
|
83
|
+
|
|
84
|
+
agent_name: str
|
|
85
|
+
"""Name of the agent that failed."""
|
|
86
|
+
message: str
|
|
87
|
+
"""Error description."""
|
|
88
|
+
exception: Exception
|
|
89
|
+
"""The exception that caused the failure."""
|
|
90
|
+
timestamp: Any = field(default_factory=get_now) # datetime
|
|
91
|
+
"""When the failure occurred."""
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class AgentReset:
|
|
95
|
+
"""Emitted when agent is reset."""
|
|
96
|
+
|
|
97
|
+
agent_name: AgentName
|
|
98
|
+
previous_tools: dict[str, bool]
|
|
99
|
+
new_tools: dict[str, bool]
|
|
100
|
+
timestamp: datetime = field(default_factory=get_now)
|
|
101
|
+
|
|
102
|
+
agent_reset = Signal[AgentReset]()
|
|
103
|
+
# Signal emitted when agent execution fails
|
|
104
|
+
run_failed: Signal[RunFailedEvent] = Signal()
|
|
105
|
+
|
|
51
106
|
def __init__(
|
|
52
107
|
self,
|
|
53
108
|
*,
|
|
@@ -64,6 +119,7 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
64
119
|
output_type: type[TResult] = str, # type: ignore[assignment]
|
|
65
120
|
tool_confirmation_mode: ToolConfirmationMode = "per_tool",
|
|
66
121
|
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
122
|
+
commands: Sequence[BaseCommand] | None = None,
|
|
67
123
|
) -> None:
|
|
68
124
|
"""Initialize base agent with shared infrastructure.
|
|
69
125
|
|
|
@@ -80,7 +136,13 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
80
136
|
output_type: Output type for this agent
|
|
81
137
|
tool_confirmation_mode: How tool execution confirmation is handled
|
|
82
138
|
event_handlers: Event handlers for this agent
|
|
139
|
+
commands: Slash commands to register with this agent
|
|
83
140
|
"""
|
|
141
|
+
from exxec import LocalExecutionEnvironment
|
|
142
|
+
from slashed import CommandStore
|
|
143
|
+
|
|
144
|
+
from agentpool_commands import get_commands
|
|
145
|
+
|
|
84
146
|
super().__init__(
|
|
85
147
|
name=name,
|
|
86
148
|
description=description,
|
|
@@ -90,10 +152,14 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
90
152
|
enable_logging=enable_logging,
|
|
91
153
|
event_configs=event_configs,
|
|
92
154
|
)
|
|
155
|
+
self._infinite = False
|
|
156
|
+
self._background_task: asyncio.Task[ChatMessage[Any]] | None = None
|
|
93
157
|
|
|
94
158
|
# Shared infrastructure - previously duplicated in all 4 agents
|
|
95
159
|
self._event_queue: asyncio.Queue[RichAgentStreamEvent[Any]] = asyncio.Queue()
|
|
96
|
-
|
|
160
|
+
# Use storage from agent_pool if available, otherwise memory-only
|
|
161
|
+
storage = agent_pool.storage if agent_pool else None
|
|
162
|
+
self.conversation = MessageHistory(storage=storage)
|
|
97
163
|
self.env = env or LocalExecutionEnvironment()
|
|
98
164
|
self._input_provider = input_provider
|
|
99
165
|
self._output_type: type[TResult] = output_type
|
|
@@ -103,10 +169,97 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
103
169
|
self.event_handler: MultiEventHandler[IndividualEventHandler] = MultiEventHandler(
|
|
104
170
|
resolved_handlers
|
|
105
171
|
)
|
|
106
|
-
|
|
107
|
-
# Cancellation infrastructure
|
|
108
172
|
self._cancelled = False
|
|
109
173
|
self._current_stream_task: asyncio.Task[Any] | None = None
|
|
174
|
+
# Deferred initialization support - subclasses set True in __aenter__,
|
|
175
|
+
# override ensure_initialized() to do actual connection
|
|
176
|
+
self._connect_pending: bool = False
|
|
177
|
+
# State change signal - emitted when mode/model/commands change
|
|
178
|
+
# Uses union type for different state update kinds
|
|
179
|
+
self.state_updated: BoundSignal[StateUpdate] = BoundSignal()
|
|
180
|
+
self._command_store: CommandStore = CommandStore()
|
|
181
|
+
# Initialize store (registers builtin help/exit commands)
|
|
182
|
+
self._command_store._initialize_sync()
|
|
183
|
+
# Register default agent commands
|
|
184
|
+
for command in get_commands():
|
|
185
|
+
self._command_store.register_command(command)
|
|
186
|
+
|
|
187
|
+
# Register additional provided commands
|
|
188
|
+
if commands:
|
|
189
|
+
for command in commands:
|
|
190
|
+
self._command_store.register_command(command)
|
|
191
|
+
|
|
192
|
+
@overload
|
|
193
|
+
def __and__( # if other doesnt define deps, we take the agents one
|
|
194
|
+
self, other: ProcessorCallback[Any] | Team[TDeps] | Agent[TDeps, Any]
|
|
195
|
+
) -> Team[TDeps]: ...
|
|
196
|
+
|
|
197
|
+
@overload
|
|
198
|
+
def __and__( # otherwise, we dont know and deps is Any
|
|
199
|
+
self, other: ProcessorCallback[Any] | Team[Any] | Agent[Any, Any]
|
|
200
|
+
) -> Team[Any]: ...
|
|
201
|
+
|
|
202
|
+
def __and__(self, other: MessageNode[Any, Any] | ProcessorCallback[Any]) -> Team[Any]:
|
|
203
|
+
"""Create sequential team using & operator.
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
group = analyzer & planner & executor # Create group of 3
|
|
207
|
+
group = analyzer & existing_group # Add to existing group
|
|
208
|
+
"""
|
|
209
|
+
from agentpool.agents.agent import Agent
|
|
210
|
+
from agentpool.delegation.team import Team
|
|
211
|
+
|
|
212
|
+
match other:
|
|
213
|
+
case Team():
|
|
214
|
+
return Team([self, *other.nodes])
|
|
215
|
+
case Callable():
|
|
216
|
+
agent_2 = Agent.from_callback(other)
|
|
217
|
+
agent_2.agent_pool = self.agent_pool
|
|
218
|
+
return Team([self, agent_2])
|
|
219
|
+
case MessageNode():
|
|
220
|
+
return Team([self, other])
|
|
221
|
+
case _:
|
|
222
|
+
msg = f"Invalid agent type: {type(other)}"
|
|
223
|
+
raise ValueError(msg)
|
|
224
|
+
|
|
225
|
+
@overload
|
|
226
|
+
def __or__(self, other: MessageNode[TDeps, Any]) -> TeamRun[TDeps, Any]: ...
|
|
227
|
+
|
|
228
|
+
@overload
|
|
229
|
+
def __or__[TOtherDeps](self, other: MessageNode[TOtherDeps, Any]) -> TeamRun[Any, Any]: ...
|
|
230
|
+
|
|
231
|
+
@overload
|
|
232
|
+
def __or__(self, other: ProcessorCallback[Any]) -> TeamRun[Any, Any]: ...
|
|
233
|
+
|
|
234
|
+
def __or__(self, other: MessageNode[Any, Any] | ProcessorCallback[Any]) -> TeamRun[Any, Any]:
|
|
235
|
+
# Create new execution with sequential mode (for piping)
|
|
236
|
+
from agentpool import TeamRun
|
|
237
|
+
from agentpool.agents.agent import Agent
|
|
238
|
+
|
|
239
|
+
if callable(other):
|
|
240
|
+
other = Agent.from_callback(other)
|
|
241
|
+
other.agent_pool = self.agent_pool
|
|
242
|
+
|
|
243
|
+
return TeamRun([self, other])
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def command_store(self) -> CommandStore:
|
|
247
|
+
"""Get the command store for slash commands."""
|
|
248
|
+
return self._command_store
|
|
249
|
+
|
|
250
|
+
async def reset(self) -> None:
|
|
251
|
+
"""Reset agent state (conversation history and tool states)."""
|
|
252
|
+
old_tools = await self.tools.list_tools()
|
|
253
|
+
await self.conversation.clear()
|
|
254
|
+
await self.tools.reset_states()
|
|
255
|
+
new_tools = await self.tools.list_tools()
|
|
256
|
+
|
|
257
|
+
event = self.AgentReset(
|
|
258
|
+
agent_name=self.name,
|
|
259
|
+
previous_tools=old_tools,
|
|
260
|
+
new_tools=new_tools,
|
|
261
|
+
)
|
|
262
|
+
await self.agent_reset.emit(event)
|
|
110
263
|
|
|
111
264
|
@abstractmethod
|
|
112
265
|
def get_context(self, data: Any = None) -> AgentContext[Any]:
|
|
@@ -127,16 +280,283 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
127
280
|
...
|
|
128
281
|
|
|
129
282
|
@abstractmethod
|
|
130
|
-
def
|
|
283
|
+
async def set_model(self, model: str) -> None:
|
|
284
|
+
"""Set the model for this agent.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
model: New model identifier to use
|
|
288
|
+
"""
|
|
289
|
+
...
|
|
290
|
+
|
|
291
|
+
async def run_iter(
|
|
131
292
|
self,
|
|
132
|
-
*
|
|
293
|
+
*prompt_groups: Sequence[PromptCompatible],
|
|
294
|
+
store_history: bool = True,
|
|
295
|
+
wait_for_connections: bool | None = None,
|
|
296
|
+
) -> AsyncIterator[ChatMessage[TResult]]:
|
|
297
|
+
"""Run agent sequentially on multiple prompt groups.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
prompt_groups: Groups of prompts to process sequentially
|
|
301
|
+
store_history: Whether to store in conversation history
|
|
302
|
+
wait_for_connections: Whether to wait for connected agents
|
|
303
|
+
|
|
304
|
+
Yields:
|
|
305
|
+
Response messages in sequence
|
|
306
|
+
|
|
307
|
+
Example:
|
|
308
|
+
questions = [
|
|
309
|
+
["What is your name?"],
|
|
310
|
+
["How old are you?", image1],
|
|
311
|
+
["Describe this image", image2],
|
|
312
|
+
]
|
|
313
|
+
async for response in agent.run_iter(*questions):
|
|
314
|
+
print(response.content)
|
|
315
|
+
"""
|
|
316
|
+
for prompts in prompt_groups:
|
|
317
|
+
response = await self.run(
|
|
318
|
+
*prompts,
|
|
319
|
+
store_history=store_history,
|
|
320
|
+
wait_for_connections=wait_for_connections,
|
|
321
|
+
)
|
|
322
|
+
yield response # pyright: ignore
|
|
323
|
+
|
|
324
|
+
async def run_in_background(
|
|
325
|
+
self,
|
|
326
|
+
*prompt: PromptCompatible,
|
|
327
|
+
max_count: int | None = None,
|
|
328
|
+
interval: float = 1.0,
|
|
133
329
|
**kwargs: Any,
|
|
330
|
+
) -> asyncio.Task[ChatMessage[TResult] | None]:
|
|
331
|
+
"""Run agent continuously in background with prompt or dynamic prompt function.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
prompt: Static prompt or function that generates prompts
|
|
335
|
+
max_count: Maximum number of runs (None = infinite)
|
|
336
|
+
interval: Seconds between runs
|
|
337
|
+
**kwargs: Arguments passed to run()
|
|
338
|
+
"""
|
|
339
|
+
self._infinite = max_count is None
|
|
340
|
+
|
|
341
|
+
async def _continuous() -> ChatMessage[Any]:
|
|
342
|
+
count = 0
|
|
343
|
+
self.log.debug("Starting continuous run", max_count=max_count, interval=interval)
|
|
344
|
+
latest = None
|
|
345
|
+
while (max_count is None or count < max_count) and not self._cancelled:
|
|
346
|
+
try:
|
|
347
|
+
agent_ctx = self.get_context()
|
|
348
|
+
current_prompts = [
|
|
349
|
+
call_with_context(p, agent_ctx, **kwargs) if callable(p) else p
|
|
350
|
+
for p in prompt
|
|
351
|
+
]
|
|
352
|
+
self.log.debug("Generated prompt", iteration=count)
|
|
353
|
+
latest = await self.run(current_prompts, **kwargs)
|
|
354
|
+
self.log.debug("Run continuous result", iteration=count)
|
|
355
|
+
|
|
356
|
+
count += 1
|
|
357
|
+
await anyio.sleep(interval)
|
|
358
|
+
except asyncio.CancelledError:
|
|
359
|
+
self.log.debug("Continuous run cancelled")
|
|
360
|
+
break
|
|
361
|
+
except Exception:
|
|
362
|
+
# Check if we were cancelled (may surface as other exceptions)
|
|
363
|
+
if self._cancelled:
|
|
364
|
+
self.log.debug("Continuous run cancelled via flag")
|
|
365
|
+
break
|
|
366
|
+
count += 1
|
|
367
|
+
self.log.exception("Background run failed")
|
|
368
|
+
await anyio.sleep(interval)
|
|
369
|
+
self.log.debug("Continuous run completed", iterations=count)
|
|
370
|
+
return latest # type: ignore[return-value]
|
|
371
|
+
|
|
372
|
+
await self.stop() # Cancel any existing background task
|
|
373
|
+
self._cancelled = False # Reset cancellation flag for new run
|
|
374
|
+
task = asyncio.create_task(_continuous(), name=f"background_{self.name}")
|
|
375
|
+
self.log.debug("Started background task", task_name=task.get_name())
|
|
376
|
+
self._background_task = task
|
|
377
|
+
return task
|
|
378
|
+
|
|
379
|
+
async def stop(self) -> None:
|
|
380
|
+
"""Stop continuous execution if running."""
|
|
381
|
+
self._cancelled = True # Signal cancellation via flag
|
|
382
|
+
if self._background_task and not self._background_task.done():
|
|
383
|
+
self._background_task.cancel()
|
|
384
|
+
with suppress(asyncio.CancelledError): # Expected when we cancel the task
|
|
385
|
+
await self._background_task
|
|
386
|
+
self._background_task = None
|
|
387
|
+
|
|
388
|
+
def is_busy(self) -> bool:
|
|
389
|
+
"""Check if agent is currently processing tasks."""
|
|
390
|
+
return bool(self.task_manager._pending_tasks or self._background_task)
|
|
391
|
+
|
|
392
|
+
async def wait(self) -> ChatMessage[TResult]:
|
|
393
|
+
"""Wait for background execution to complete."""
|
|
394
|
+
if not self._background_task:
|
|
395
|
+
msg = "No background task running"
|
|
396
|
+
raise RuntimeError(msg)
|
|
397
|
+
if self._infinite:
|
|
398
|
+
msg = "Cannot wait on infinite execution"
|
|
399
|
+
raise RuntimeError(msg)
|
|
400
|
+
try:
|
|
401
|
+
return await self._background_task
|
|
402
|
+
finally:
|
|
403
|
+
self._background_task = None
|
|
404
|
+
|
|
405
|
+
@method_spawner
|
|
406
|
+
async def run_stream(
|
|
407
|
+
self,
|
|
408
|
+
*prompts: PromptCompatible,
|
|
409
|
+
store_history: bool = True,
|
|
410
|
+
message_id: str | None = None,
|
|
411
|
+
conversation_id: str | None = None,
|
|
412
|
+
parent_id: str | None = None,
|
|
413
|
+
message_history: MessageHistory | None = None,
|
|
414
|
+
input_provider: InputProvider | None = None,
|
|
415
|
+
wait_for_connections: bool | None = None,
|
|
416
|
+
deps: TDeps | None = None,
|
|
417
|
+
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
134
418
|
) -> AsyncIterator[RichAgentStreamEvent[TResult]]:
|
|
135
419
|
"""Run agent with streaming output.
|
|
136
420
|
|
|
421
|
+
This method delegates to _stream_events() which must be implemented by subclasses.
|
|
422
|
+
Handles prompt conversion from various formats to UserContent.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
*prompts: Input prompts (various formats supported)
|
|
426
|
+
store_history: Whether to store in history
|
|
427
|
+
message_id: Optional message ID
|
|
428
|
+
conversation_id: Optional conversation ID
|
|
429
|
+
parent_id: Optional parent message ID
|
|
430
|
+
message_history: Optional message history
|
|
431
|
+
input_provider: Optional input provider
|
|
432
|
+
wait_for_connections: Whether to wait for connected agents
|
|
433
|
+
deps: Optional dependencies
|
|
434
|
+
event_handlers: Optional event handlers
|
|
435
|
+
|
|
436
|
+
Yields:
|
|
437
|
+
Stream events during execution
|
|
438
|
+
"""
|
|
439
|
+
from agentpool.messaging import ChatMessage
|
|
440
|
+
from agentpool.utils.identifiers import generate_session_id
|
|
441
|
+
|
|
442
|
+
# Convert prompts to standard UserContent format
|
|
443
|
+
converted_prompts = await convert_prompts(prompts)
|
|
444
|
+
# Get message history (either passed or agent's own)
|
|
445
|
+
conversation = message_history if message_history is not None else self.conversation
|
|
446
|
+
# Determine effective parent_id (from param or last message in history)
|
|
447
|
+
effective_parent_id = parent_id if parent_id else conversation.get_last_message_id()
|
|
448
|
+
# Initialize or adopt conversation_id
|
|
449
|
+
if self.conversation_id is None:
|
|
450
|
+
if conversation_id:
|
|
451
|
+
# Adopt conversation_id (from agent chain or external session like ACP)
|
|
452
|
+
self.conversation_id = conversation_id
|
|
453
|
+
else:
|
|
454
|
+
# Generate new conversation_id
|
|
455
|
+
self.conversation_id = generate_session_id()
|
|
456
|
+
# Always log conversation with initial prompt for title generation
|
|
457
|
+
# StorageManager handles idempotent behavior (skip if already logged)
|
|
458
|
+
# Use last prompt to avoid staged content (staged is prepended, user prompt is last)
|
|
459
|
+
user_prompts = [
|
|
460
|
+
str(p) for p in prompts if isinstance(p, str)
|
|
461
|
+
] # Filter to text prompts only
|
|
462
|
+
initial_prompt = user_prompts[-1] if user_prompts else None
|
|
463
|
+
await self.log_conversation(initial_prompt)
|
|
464
|
+
elif conversation_id and self.conversation_id != conversation_id:
|
|
465
|
+
# Adopt passed conversation_id (for routing chains)
|
|
466
|
+
self.conversation_id = conversation_id
|
|
467
|
+
|
|
468
|
+
user_msg = ChatMessage.user_prompt(
|
|
469
|
+
message=converted_prompts,
|
|
470
|
+
parent_id=effective_parent_id,
|
|
471
|
+
conversation_id=self.conversation_id,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# Stream events from implementation
|
|
475
|
+
final_message = None
|
|
476
|
+
self._current_stream_task = asyncio.current_task()
|
|
477
|
+
try:
|
|
478
|
+
async for event in self._stream_events(
|
|
479
|
+
converted_prompts,
|
|
480
|
+
user_msg=user_msg,
|
|
481
|
+
effective_parent_id=effective_parent_id,
|
|
482
|
+
store_history=store_history,
|
|
483
|
+
message_id=message_id,
|
|
484
|
+
conversation_id=conversation_id,
|
|
485
|
+
parent_id=parent_id,
|
|
486
|
+
message_history=message_history,
|
|
487
|
+
input_provider=input_provider,
|
|
488
|
+
wait_for_connections=wait_for_connections,
|
|
489
|
+
deps=deps,
|
|
490
|
+
event_handlers=event_handlers,
|
|
491
|
+
):
|
|
492
|
+
yield event
|
|
493
|
+
# Capture final message from StreamCompleteEvent
|
|
494
|
+
if isinstance(event, StreamCompleteEvent):
|
|
495
|
+
final_message = event.message
|
|
496
|
+
except Exception as e:
|
|
497
|
+
self.log.exception("Agent stream failed")
|
|
498
|
+
failed_event = BaseAgent.RunFailedEvent(
|
|
499
|
+
agent_name=self.name,
|
|
500
|
+
message="Agent stream failed",
|
|
501
|
+
exception=e,
|
|
502
|
+
)
|
|
503
|
+
await self.run_failed.emit(failed_event)
|
|
504
|
+
raise
|
|
505
|
+
finally:
|
|
506
|
+
self._current_stream_task = None
|
|
507
|
+
|
|
508
|
+
# Post-processing after stream completes
|
|
509
|
+
if final_message is not None:
|
|
510
|
+
# Emit signal (always - for event handlers)
|
|
511
|
+
await self.message_sent.emit(final_message)
|
|
512
|
+
# Conditional persistence based on store_history
|
|
513
|
+
# TODO: Verify store_history semantics across all use cases:
|
|
514
|
+
# - Should subagent tool calls set store_history=False?
|
|
515
|
+
# - Should forked/ephemeral runs always skip persistence?
|
|
516
|
+
# - Should signals still fire when store_history=False?
|
|
517
|
+
# Current behavior: store_history controls both DB logging AND conversation context
|
|
518
|
+
if store_history:
|
|
519
|
+
# Log to persistent storage and add to conversation context
|
|
520
|
+
await self.log_message(final_message)
|
|
521
|
+
conversation.add_chat_messages([user_msg, final_message])
|
|
522
|
+
# Route to connected agents (always - they decide what to do with it)
|
|
523
|
+
await self.connections.route_message(final_message, wait=wait_for_connections)
|
|
524
|
+
|
|
525
|
+
@abstractmethod
|
|
526
|
+
def _stream_events(
|
|
527
|
+
self,
|
|
528
|
+
prompts: list[UserContent],
|
|
529
|
+
*,
|
|
530
|
+
user_msg: Any, # ChatMessage but imported in run_stream
|
|
531
|
+
effective_parent_id: str | None,
|
|
532
|
+
message_id: str | None = None,
|
|
533
|
+
conversation_id: str | None = None,
|
|
534
|
+
parent_id: str | None = None,
|
|
535
|
+
input_provider: InputProvider | None = None,
|
|
536
|
+
message_history: MessageHistory | None = None,
|
|
537
|
+
deps: TDeps | None = None,
|
|
538
|
+
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
539
|
+
wait_for_connections: bool | None = None,
|
|
540
|
+
store_history: bool = True,
|
|
541
|
+
) -> AsyncIterator[RichAgentStreamEvent[TResult]]:
|
|
542
|
+
"""Agent-specific streaming implementation.
|
|
543
|
+
|
|
544
|
+
Subclasses must implement this to provide their streaming logic.
|
|
545
|
+
Prompts are pre-converted to UserContent format by run_stream().
|
|
546
|
+
|
|
137
547
|
Args:
|
|
138
|
-
|
|
139
|
-
|
|
548
|
+
prompts: Converted prompts in UserContent format
|
|
549
|
+
user_msg: Pre-created user ChatMessage (from base class)
|
|
550
|
+
effective_parent_id: Resolved parent message ID for threading
|
|
551
|
+
message_id: Optional message ID
|
|
552
|
+
conversation_id: Optional conversation ID
|
|
553
|
+
parent_id: Optional parent message ID
|
|
554
|
+
input_provider: Optional input provider
|
|
555
|
+
message_history: Optional message history
|
|
556
|
+
deps: Optional dependencies
|
|
557
|
+
event_handlers: Optional event handlers
|
|
558
|
+
wait_for_connections: Whether to wait for connected agents
|
|
559
|
+
store_history: Whether to store in history
|
|
140
560
|
|
|
141
561
|
Yields:
|
|
142
562
|
Stream events during execution
|
|
@@ -151,6 +571,25 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
151
571
|
"""
|
|
152
572
|
self.tool_confirmation_mode = mode
|
|
153
573
|
|
|
574
|
+
def is_initializing(self) -> bool:
|
|
575
|
+
"""Check if agent is still initializing.
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
True if deferred initialization is pending
|
|
579
|
+
"""
|
|
580
|
+
return self._connect_pending
|
|
581
|
+
|
|
582
|
+
async def ensure_initialized(self) -> None:
|
|
583
|
+
"""Wait for deferred initialization to complete.
|
|
584
|
+
|
|
585
|
+
Subclasses that use deferred init should:
|
|
586
|
+
1. Set `self._connect_pending = True` in `__aenter__`
|
|
587
|
+
2. Override this method to do actual connection work
|
|
588
|
+
3. Set `self._connect_pending = False` when done
|
|
589
|
+
|
|
590
|
+
The base implementation is a no-op for agents without deferred init.
|
|
591
|
+
"""
|
|
592
|
+
|
|
154
593
|
def is_cancelled(self) -> bool:
|
|
155
594
|
"""Check if the agent has been cancelled.
|
|
156
595
|
|
|
@@ -175,3 +614,179 @@ class BaseAgent[TDeps = None, TResult = str](MessageNode[TDeps, TResult]):
|
|
|
175
614
|
if self._current_stream_task and not self._current_stream_task.done():
|
|
176
615
|
self._current_stream_task.cancel()
|
|
177
616
|
logger.info("Interrupted agent stream", agent=self.name)
|
|
617
|
+
|
|
618
|
+
async def get_stats(self) -> MessageStats:
|
|
619
|
+
"""Get message statistics."""
|
|
620
|
+
from agentpool.talk.stats import MessageStats
|
|
621
|
+
|
|
622
|
+
return MessageStats(messages=list(self.conversation.chat_messages))
|
|
623
|
+
|
|
624
|
+
def get_mcp_server_info(self) -> dict[str, MCPServerStatus]:
|
|
625
|
+
"""Get information about configured MCP servers.
|
|
626
|
+
|
|
627
|
+
Returns a dict mapping server names to their status info. Used by
|
|
628
|
+
the OpenCode /mcp endpoint to display MCP servers in the UI.
|
|
629
|
+
|
|
630
|
+
The default implementation checks external_providers on the tool manager.
|
|
631
|
+
Subclasses may override to provide agent-specific MCP server info
|
|
632
|
+
(e.g., ClaudeCodeAgent has its own MCP server handling).
|
|
633
|
+
|
|
634
|
+
Returns:
|
|
635
|
+
Dict mapping server name to MCPServerStatus
|
|
636
|
+
"""
|
|
637
|
+
from agentpool.common_types import MCPServerStatus
|
|
638
|
+
from agentpool.mcp_server.manager import MCPManager
|
|
639
|
+
from agentpool.resource_providers import AggregatingResourceProvider
|
|
640
|
+
from agentpool.resource_providers.mcp_provider import MCPResourceProvider
|
|
641
|
+
|
|
642
|
+
def add_status(provider: MCPResourceProvider, result: dict[str, MCPServerStatus]) -> None:
|
|
643
|
+
status_dict = provider.get_status()
|
|
644
|
+
status_type = status_dict.get("status", "disabled")
|
|
645
|
+
if status_type == "connected":
|
|
646
|
+
result[provider.name] = MCPServerStatus(
|
|
647
|
+
name=provider.name, status="connected", server_type="stdio"
|
|
648
|
+
)
|
|
649
|
+
elif status_type == "failed":
|
|
650
|
+
error = status_dict.get("error", "Unknown error")
|
|
651
|
+
result[provider.name] = MCPServerStatus(
|
|
652
|
+
name=provider.name, status="error", error=error
|
|
653
|
+
)
|
|
654
|
+
else:
|
|
655
|
+
result[provider.name] = MCPServerStatus(name=provider.name, status="disconnected")
|
|
656
|
+
|
|
657
|
+
result: dict[str, MCPServerStatus] = {}
|
|
658
|
+
try:
|
|
659
|
+
for provider in self.tools.external_providers:
|
|
660
|
+
if isinstance(provider, MCPResourceProvider):
|
|
661
|
+
add_status(provider, result)
|
|
662
|
+
elif isinstance(provider, AggregatingResourceProvider):
|
|
663
|
+
for nested in provider.providers:
|
|
664
|
+
if isinstance(nested, MCPResourceProvider):
|
|
665
|
+
add_status(nested, result)
|
|
666
|
+
elif isinstance(provider, MCPManager):
|
|
667
|
+
for mcp_provider in provider.get_mcp_providers():
|
|
668
|
+
add_status(mcp_provider, result)
|
|
669
|
+
except Exception: # noqa: BLE001
|
|
670
|
+
pass
|
|
671
|
+
|
|
672
|
+
return result
|
|
673
|
+
|
|
674
|
+
@method_spawner
|
|
675
|
+
async def run(
|
|
676
|
+
self,
|
|
677
|
+
*prompts: PromptCompatible | ChatMessage[Any],
|
|
678
|
+
store_history: bool = True,
|
|
679
|
+
message_id: str | None = None,
|
|
680
|
+
conversation_id: str | None = None,
|
|
681
|
+
parent_id: str | None = None,
|
|
682
|
+
message_history: MessageHistory | None = None,
|
|
683
|
+
deps: TDeps | None = None,
|
|
684
|
+
input_provider: InputProvider | None = None,
|
|
685
|
+
event_handlers: Sequence[IndividualEventHandler | BuiltinEventHandlerType] | None = None,
|
|
686
|
+
wait_for_connections: bool | None = None,
|
|
687
|
+
) -> ChatMessage[TResult]:
|
|
688
|
+
"""Run agent with prompt and get response.
|
|
689
|
+
|
|
690
|
+
This is the standard synchronous run method shared by all agent types.
|
|
691
|
+
It collects all streaming events from run_stream() and returns the final message.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
prompts: User query or instruction
|
|
695
|
+
store_history: Whether the message exchange should be added to the
|
|
696
|
+
context window
|
|
697
|
+
message_id: Optional message id for the returned message.
|
|
698
|
+
Automatically generated if not provided.
|
|
699
|
+
conversation_id: Optional conversation id for the returned message.
|
|
700
|
+
parent_id: Parent message id
|
|
701
|
+
message_history: Optional MessageHistory object to
|
|
702
|
+
use instead of agent's own conversation
|
|
703
|
+
deps: Optional dependencies for the agent
|
|
704
|
+
input_provider: Optional input provider for the agent
|
|
705
|
+
event_handlers: Optional event handlers for this run (overrides agent's handlers)
|
|
706
|
+
wait_for_connections: Whether to wait for connected agents to complete
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
ChatMessage containing response and run information
|
|
710
|
+
|
|
711
|
+
Raises:
|
|
712
|
+
RuntimeError: If no final message received from stream
|
|
713
|
+
UnexpectedModelBehavior: If the model fails or behaves unexpectedly
|
|
714
|
+
"""
|
|
715
|
+
# Collect all events through run_stream
|
|
716
|
+
final_message: ChatMessage[TResult] | None = None
|
|
717
|
+
async for event in self.run_stream(
|
|
718
|
+
*prompts,
|
|
719
|
+
store_history=store_history,
|
|
720
|
+
message_id=message_id,
|
|
721
|
+
conversation_id=conversation_id,
|
|
722
|
+
parent_id=parent_id,
|
|
723
|
+
message_history=message_history,
|
|
724
|
+
deps=deps,
|
|
725
|
+
input_provider=input_provider,
|
|
726
|
+
event_handlers=event_handlers,
|
|
727
|
+
wait_for_connections=wait_for_connections,
|
|
728
|
+
):
|
|
729
|
+
if isinstance(event, StreamCompleteEvent):
|
|
730
|
+
final_message = event.message
|
|
731
|
+
|
|
732
|
+
if final_message is None:
|
|
733
|
+
msg = "No final message received from stream"
|
|
734
|
+
raise RuntimeError(msg)
|
|
735
|
+
|
|
736
|
+
return final_message
|
|
737
|
+
|
|
738
|
+
@abstractmethod
|
|
739
|
+
async def get_available_models(self) -> list[ModelInfo] | None:
|
|
740
|
+
"""Get available models for this agent.
|
|
741
|
+
|
|
742
|
+
Returns a list of models that can be used with this agent, or None
|
|
743
|
+
if model discovery is not supported for this agent type.
|
|
744
|
+
|
|
745
|
+
Uses tokonomics.ModelInfo which includes pricing, capabilities,
|
|
746
|
+
and limits. Can be converted to protocol-specific formats (OpenCode, ACP).
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
List of tokonomics ModelInfo, or None if not supported
|
|
750
|
+
"""
|
|
751
|
+
...
|
|
752
|
+
|
|
753
|
+
@abstractmethod
|
|
754
|
+
async def get_modes(self) -> list[ModeCategory]:
|
|
755
|
+
"""Get available mode categories for this agent.
|
|
756
|
+
|
|
757
|
+
Returns a list of mode categories that can be switched. Each category
|
|
758
|
+
represents a group of mutually exclusive modes (e.g., permissions,
|
|
759
|
+
models, behavior presets).
|
|
760
|
+
|
|
761
|
+
Different agent types expose different modes:
|
|
762
|
+
- Native Agent: permissions + model selection
|
|
763
|
+
- ClaudeCodeAgent: permissions + model selection
|
|
764
|
+
- ACPAgent: Passthrough from remote server
|
|
765
|
+
- AGUIAgent: model selection (if applicable)
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
List of ModeCategory, empty list if no modes supported
|
|
769
|
+
"""
|
|
770
|
+
...
|
|
771
|
+
|
|
772
|
+
@abstractmethod
|
|
773
|
+
async def set_mode(self, mode: ModeInfo | str, category_id: str | None = None) -> None:
|
|
774
|
+
"""Set a mode within a category.
|
|
775
|
+
|
|
776
|
+
Each agent type handles mode switching according to its own semantics:
|
|
777
|
+
- Native Agent: Maps to tool confirmation mode
|
|
778
|
+
- ClaudeCodeAgent: Maps to SDK permission mode
|
|
779
|
+
- ACPAgent: Forwards to remote server
|
|
780
|
+
- AGUIAgent: No-op (no modes supported)
|
|
781
|
+
|
|
782
|
+
Args:
|
|
783
|
+
mode: The mode to activate - either a ModeInfo object or mode ID string.
|
|
784
|
+
If ModeInfo, category_id is extracted from it (unless overridden).
|
|
785
|
+
category_id: Optional category ID. If None and mode is a string,
|
|
786
|
+
uses the first category. If None and mode is ModeInfo,
|
|
787
|
+
uses the mode's category_id.
|
|
788
|
+
|
|
789
|
+
Raises:
|
|
790
|
+
ValueError: If mode_id or category_id is invalid
|
|
791
|
+
"""
|
|
792
|
+
...
|