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,307 @@
|
|
|
1
|
+
from __future__ import annotations, annotations as _annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from functools import partial
|
|
6
|
+
from typing import TYPE_CHECKING, Any, TypeVar, assert_never
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
from fasta2a.applications import FastA2A # type: ignore[import-untyped]
|
|
10
|
+
from fasta2a.broker import InMemoryBroker # type: ignore[import-untyped]
|
|
11
|
+
from fasta2a.schema import ( # type: ignore[import-untyped]
|
|
12
|
+
Artifact,
|
|
13
|
+
DataPart,
|
|
14
|
+
Message,
|
|
15
|
+
TextPart as A2ATextPart,
|
|
16
|
+
)
|
|
17
|
+
from fasta2a.storage import InMemoryStorage # type: ignore[import-untyped]
|
|
18
|
+
from fasta2a.worker import Worker # type: ignore[import-untyped]
|
|
19
|
+
from pydantic import TypeAdapter
|
|
20
|
+
from pydantic_ai import (
|
|
21
|
+
AudioUrl,
|
|
22
|
+
BinaryContent,
|
|
23
|
+
DocumentUrl,
|
|
24
|
+
ImageUrl,
|
|
25
|
+
ModelMessage,
|
|
26
|
+
ModelRequest,
|
|
27
|
+
ModelResponse,
|
|
28
|
+
TextPart,
|
|
29
|
+
ThinkingPart,
|
|
30
|
+
ToolCallPart,
|
|
31
|
+
UserPromptPart,
|
|
32
|
+
VideoUrl,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from collections.abc import AsyncIterator, Sequence
|
|
38
|
+
|
|
39
|
+
from fasta2a.broker import Broker
|
|
40
|
+
from fasta2a.schema import AgentProvider, Part, Skill, TaskIdParams, TaskSendParams
|
|
41
|
+
from fasta2a.storage import Storage
|
|
42
|
+
from pydantic_ai import ModelRequestPart, ModelResponsePart, UserContent
|
|
43
|
+
from starlette.middleware import Middleware
|
|
44
|
+
from starlette.routing import Route
|
|
45
|
+
from starlette.types import ExceptionHandler, Lifespan
|
|
46
|
+
|
|
47
|
+
from agentpool.agents.base_agent import BaseAgent
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# AgentWorker output type needs to be invariant for use in both parameter and return positions
|
|
51
|
+
WorkerOutputT = TypeVar("WorkerOutputT")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@asynccontextmanager
|
|
55
|
+
async def worker_lifespan(app: FastA2A, worker: Worker, agent: BaseAgent) -> AsyncIterator[None]:
|
|
56
|
+
"""Custom lifespan that runs the worker during application startup.
|
|
57
|
+
|
|
58
|
+
This ensures the worker is started and ready to process tasks as soon as the application starts.
|
|
59
|
+
"""
|
|
60
|
+
async with app.task_manager, agent, worker.run():
|
|
61
|
+
yield
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def agent_to_a2a(
|
|
65
|
+
agent: BaseAgent,
|
|
66
|
+
*,
|
|
67
|
+
storage: Storage | None = None,
|
|
68
|
+
broker: Broker | None = None,
|
|
69
|
+
# Agent card
|
|
70
|
+
name: str | None = None,
|
|
71
|
+
url: str = "http://localhost:8000",
|
|
72
|
+
version: str = "1.0.0",
|
|
73
|
+
description: str | None = None,
|
|
74
|
+
provider: AgentProvider | None = None,
|
|
75
|
+
skills: list[Skill] | None = None,
|
|
76
|
+
# Starlette
|
|
77
|
+
debug: bool = False,
|
|
78
|
+
routes: Sequence[Route] | None = None,
|
|
79
|
+
middleware: Sequence[Middleware] | None = None,
|
|
80
|
+
exception_handlers: dict[Any, ExceptionHandler] | None = None,
|
|
81
|
+
lifespan: Lifespan[FastA2A] | None = None,
|
|
82
|
+
) -> FastA2A:
|
|
83
|
+
"""Create a FastA2A server from an agent."""
|
|
84
|
+
storage = storage or InMemoryStorage()
|
|
85
|
+
broker = broker or InMemoryBroker()
|
|
86
|
+
worker = AgentWorker(agent=agent, broker=broker, storage=storage)
|
|
87
|
+
lifespan = lifespan or partial(worker_lifespan, worker=worker, agent=agent)
|
|
88
|
+
return FastA2A(
|
|
89
|
+
storage=storage,
|
|
90
|
+
broker=broker,
|
|
91
|
+
name=name or agent.name,
|
|
92
|
+
url=url,
|
|
93
|
+
version=version,
|
|
94
|
+
description=description,
|
|
95
|
+
provider=provider,
|
|
96
|
+
skills=skills,
|
|
97
|
+
debug=debug,
|
|
98
|
+
routes=routes,
|
|
99
|
+
middleware=middleware,
|
|
100
|
+
exception_handlers=exception_handlers,
|
|
101
|
+
lifespan=lifespan,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class AgentWorker[WorkerOutputT, AgentDepsT](Worker[list[ModelMessage]]): # type: ignore[misc]
|
|
107
|
+
"""A worker that uses an agent to execute tasks."""
|
|
108
|
+
|
|
109
|
+
agent: BaseAgent[AgentDepsT, WorkerOutputT]
|
|
110
|
+
|
|
111
|
+
async def run_task(self, params: TaskSendParams) -> None:
|
|
112
|
+
task = await self.storage.load_task(params["id"])
|
|
113
|
+
if task is None:
|
|
114
|
+
raise ValueError(f"Task {params['id']} not found") # pragma: no cover
|
|
115
|
+
|
|
116
|
+
# TODO(Marcelo): Should we lock `run_task` on the `context_id`?
|
|
117
|
+
# Ensure this task hasn't been run before
|
|
118
|
+
if task["status"]["state"] != "submitted":
|
|
119
|
+
raise ValueError( # pragma: no cover
|
|
120
|
+
f"Task {params['id']} has already been processed (state: {task['status']['state']})"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
await self.storage.update_task(task["id"], state="working")
|
|
124
|
+
|
|
125
|
+
# Load context - contains pydantic-ai msg history from previous tasks in this conversation
|
|
126
|
+
message_history = await self.storage.load_context(task["context_id"]) or []
|
|
127
|
+
message_history.extend(self.build_message_history(task.get("history", [])))
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
result = await self.agent.run(message_history=message_history) # type: ignore
|
|
131
|
+
messages = [
|
|
132
|
+
msg
|
|
133
|
+
for chat_msg in self.agent.conversation.chat_messages
|
|
134
|
+
for msg in chat_msg.messages
|
|
135
|
+
]
|
|
136
|
+
await self.storage.update_context(task["context_id"], messages)
|
|
137
|
+
# Convert new messages to A2A format for task history
|
|
138
|
+
a2a_messages: list[Message] = []
|
|
139
|
+
for message in result.messages:
|
|
140
|
+
if isinstance(message, ModelRequest):
|
|
141
|
+
# Skip user prompts - they're already in task history
|
|
142
|
+
continue
|
|
143
|
+
# Convert response parts to A2A format
|
|
144
|
+
a2a_parts = self._response_parts_to_a2a(message.parts)
|
|
145
|
+
if a2a_parts: # Add if there are visible parts (text/thinking)
|
|
146
|
+
a2a_messages.append(
|
|
147
|
+
Message(
|
|
148
|
+
role="agent",
|
|
149
|
+
parts=a2a_parts,
|
|
150
|
+
kind="message",
|
|
151
|
+
message_id=str(uuid.uuid4()),
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
artifacts = self.build_artifacts(result.data)
|
|
156
|
+
except Exception:
|
|
157
|
+
await self.storage.update_task(task["id"], state="failed")
|
|
158
|
+
raise
|
|
159
|
+
else:
|
|
160
|
+
await self.storage.update_task(
|
|
161
|
+
task["id"], state="completed", new_artifacts=artifacts, new_messages=a2a_messages
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
async def cancel_task(self, params: TaskIdParams) -> None:
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
def build_artifacts(self, result: WorkerOutputT) -> list[Artifact]:
|
|
168
|
+
"""Build artifacts from agent result.
|
|
169
|
+
|
|
170
|
+
All agent outputs become artifacts to mark them as durable task outputs.
|
|
171
|
+
For string results, we use TextPart. For structured data, we use DataPart.
|
|
172
|
+
Metadata is included to preserve type information.
|
|
173
|
+
"""
|
|
174
|
+
artifact_id = str(uuid.uuid4())
|
|
175
|
+
part = self._convert_result_to_part(result)
|
|
176
|
+
return [Artifact(artifact_id=artifact_id, name="result", parts=[part])]
|
|
177
|
+
|
|
178
|
+
def _convert_result_to_part(self, result: WorkerOutputT) -> Part:
|
|
179
|
+
"""Convert agent result to a Part (TextPart or DataPart).
|
|
180
|
+
|
|
181
|
+
For string results, returns a TextPart.
|
|
182
|
+
For structured data, returns a DataPart with properly serialized data.
|
|
183
|
+
"""
|
|
184
|
+
if isinstance(result, str):
|
|
185
|
+
return A2ATextPart(kind="text", text=result)
|
|
186
|
+
output_type = type(result)
|
|
187
|
+
type_adapter = TypeAdapter(output_type)
|
|
188
|
+
data = type_adapter.dump_python(result, mode="json")
|
|
189
|
+
json_schema = type_adapter.json_schema(mode="serialization")
|
|
190
|
+
return DataPart(kind="data", data={"result": data}, metadata={"json_schema": json_schema})
|
|
191
|
+
|
|
192
|
+
def build_message_history(self, history: list[Message]) -> list[ModelMessage]:
|
|
193
|
+
model_messages: list[ModelMessage] = []
|
|
194
|
+
for message in history:
|
|
195
|
+
if message["role"] == "user":
|
|
196
|
+
model_messages.append(
|
|
197
|
+
ModelRequest(parts=self._request_parts_from_a2a(message["parts"]))
|
|
198
|
+
)
|
|
199
|
+
else:
|
|
200
|
+
model_messages.append(
|
|
201
|
+
ModelResponse(parts=self._response_parts_from_a2a(message["parts"]))
|
|
202
|
+
)
|
|
203
|
+
return model_messages
|
|
204
|
+
|
|
205
|
+
def _request_parts_from_a2a(self, parts: list[Part]) -> list[ModelRequestPart]:
|
|
206
|
+
"""Convert A2A Part objects to pydantic-ai ModelRequestPart objects.
|
|
207
|
+
|
|
208
|
+
This handles the conversion from A2A protocol parts (text, file, data) to
|
|
209
|
+
pydantic-ai's internal request parts (UserPromptPart with various content types).
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
parts: List of A2A Part objects from incoming messages
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
List of ModelRequestPart objects for the pydantic-ai agent
|
|
216
|
+
"""
|
|
217
|
+
model_parts: list[ModelRequestPart] = []
|
|
218
|
+
for part in parts:
|
|
219
|
+
if part["kind"] == "text":
|
|
220
|
+
model_parts.append(UserPromptPart(content=part["text"]))
|
|
221
|
+
elif part["kind"] == "file":
|
|
222
|
+
file_content = part["file"]
|
|
223
|
+
if "bytes" in file_content:
|
|
224
|
+
data = file_content["bytes"].encode("utf-8")
|
|
225
|
+
mime_type = file_content.get("mime_type", "application/octet-stream")
|
|
226
|
+
content: UserContent = BinaryContent(data=data, media_type=mime_type)
|
|
227
|
+
model_parts.append(UserPromptPart(content=[content]))
|
|
228
|
+
else:
|
|
229
|
+
url = file_content["uri"]
|
|
230
|
+
for url_cls in (DocumentUrl, AudioUrl, ImageUrl, VideoUrl):
|
|
231
|
+
content = url_cls(url=url)
|
|
232
|
+
try:
|
|
233
|
+
content.media_type # noqa: B018
|
|
234
|
+
except ValueError: # pragma: no cover
|
|
235
|
+
continue
|
|
236
|
+
else:
|
|
237
|
+
break
|
|
238
|
+
else:
|
|
239
|
+
raise ValueError(f"Unsupported file type: {url}") # pragma: no cover
|
|
240
|
+
model_parts.append(UserPromptPart(content=[content]))
|
|
241
|
+
elif part["kind"] == "data":
|
|
242
|
+
raise NotImplementedError("Data parts are not supported yet.")
|
|
243
|
+
else:
|
|
244
|
+
assert_never(part)
|
|
245
|
+
return model_parts
|
|
246
|
+
|
|
247
|
+
def _response_parts_from_a2a(self, parts: list[Part]) -> list[ModelResponsePart]:
|
|
248
|
+
"""Convert A2A Part objects to pydantic-ai ModelResponsePart objects.
|
|
249
|
+
|
|
250
|
+
This handles the conversion from A2A protocol parts (text, file, data) to
|
|
251
|
+
pydantic-ai's internal response parts. Currently only supports text parts
|
|
252
|
+
as agent responses in A2A are expected to be text-based.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
parts: List of A2A Part objects from stored agent messages
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
List of ModelResponsePart objects for message history
|
|
259
|
+
"""
|
|
260
|
+
model_parts: list[ModelResponsePart] = []
|
|
261
|
+
for part in parts:
|
|
262
|
+
if part["kind"] == "text":
|
|
263
|
+
model_parts.append(TextPart(content=part["text"]))
|
|
264
|
+
elif part["kind"] == "file": # pragma: no cover
|
|
265
|
+
raise NotImplementedError("File parts are not supported yet.")
|
|
266
|
+
elif part["kind"] == "data": # pragma: no cover
|
|
267
|
+
raise NotImplementedError("Data parts are not supported yet.")
|
|
268
|
+
else: # pragma: no cover
|
|
269
|
+
assert_never(part)
|
|
270
|
+
return model_parts
|
|
271
|
+
|
|
272
|
+
def _response_parts_to_a2a(self, parts: Sequence[ModelResponsePart]) -> list[Part]:
|
|
273
|
+
"""Convert pydantic-ai ModelResponsePart objects to A2A Part objects.
|
|
274
|
+
|
|
275
|
+
This handles the conversion from pydantic-ai's internal response parts to
|
|
276
|
+
A2A protocol parts. Different part types are handled as follows:
|
|
277
|
+
- TextPart: Converted directly to A2A TextPart
|
|
278
|
+
- ThinkingPart: Converted to TextPart with metadata indicating it's thinking
|
|
279
|
+
- ToolCallPart: Skipped (internal to agent execution)
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
parts: List of ModelResponsePart objects from agent response
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
List of A2A Part objects suitable for sending via A2A protocol
|
|
286
|
+
"""
|
|
287
|
+
a2a_parts: list[Part] = []
|
|
288
|
+
for part in parts:
|
|
289
|
+
if isinstance(part, TextPart):
|
|
290
|
+
a2a_parts.append(A2ATextPart(kind="text", text=part.content))
|
|
291
|
+
elif isinstance(part, ThinkingPart):
|
|
292
|
+
# Convert thinking to text with metadata
|
|
293
|
+
a2a_parts.append(
|
|
294
|
+
A2ATextPart(
|
|
295
|
+
kind="text",
|
|
296
|
+
text=part.content,
|
|
297
|
+
metadata={
|
|
298
|
+
"type": "thinking",
|
|
299
|
+
"thinking_id": part.id,
|
|
300
|
+
"signature": part.signature,
|
|
301
|
+
},
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
elif isinstance(part, ToolCallPart):
|
|
305
|
+
# Skip tool calls - they're internal to agent execution
|
|
306
|
+
pass
|
|
307
|
+
return a2a_parts
|
|
@@ -72,8 +72,16 @@ class A2AServer(HTTPServer):
|
|
|
72
72
|
Returns:
|
|
73
73
|
List of Route objects for each agent plus root listing endpoint
|
|
74
74
|
"""
|
|
75
|
+
from functools import partial
|
|
76
|
+
|
|
77
|
+
from fasta2a import FastA2A # type: ignore[import-untyped]
|
|
78
|
+
from fasta2a.broker import InMemoryBroker # type: ignore[import-untyped]
|
|
79
|
+
from fasta2a.storage import InMemoryStorage # type: ignore[import-untyped]
|
|
80
|
+
from starlette.responses import JSONResponse, Response
|
|
75
81
|
from starlette.routing import Route
|
|
76
82
|
|
|
83
|
+
from agentpool_server.a2a_server.agent_worker import AgentWorker, worker_lifespan
|
|
84
|
+
|
|
77
85
|
routes: list[Route] = []
|
|
78
86
|
|
|
79
87
|
# Create route for each agent in the pool
|
|
@@ -81,33 +89,30 @@ class A2AServer(HTTPServer):
|
|
|
81
89
|
|
|
82
90
|
async def agent_handler(request: Request, agent_name: str = agent_name) -> Response:
|
|
83
91
|
"""Handle A2A requests for a specific agent."""
|
|
84
|
-
from starlette.responses import JSONResponse
|
|
85
|
-
|
|
86
92
|
try:
|
|
87
93
|
# Get the agent from pool
|
|
88
|
-
|
|
89
|
-
if
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
status_code=404,
|
|
93
|
-
)
|
|
94
|
-
|
|
94
|
+
agent = self.pool.agents.get(agent_name)
|
|
95
|
+
if agent is None:
|
|
96
|
+
error = {"error": f"Agent '{agent_name}' not found"}
|
|
97
|
+
return JSONResponse(error, status_code=404)
|
|
95
98
|
# Get the underlying pydantic-ai agentlet and convert to A2A app
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
storage = InMemoryStorage()
|
|
100
|
+
broker = InMemoryBroker()
|
|
101
|
+
worker = AgentWorker(agent=agent, broker=broker, storage=storage)
|
|
102
|
+
lifespan = partial(worker_lifespan, worker=worker, agent=agent)
|
|
103
|
+
a2a_app = FastA2A(
|
|
104
|
+
storage=storage,
|
|
105
|
+
broker=broker,
|
|
106
|
+
name=agent.name,
|
|
107
|
+
lifespan=lifespan,
|
|
108
|
+
)
|
|
99
109
|
# ASGI apps don't return a value, they write to send()
|
|
100
110
|
await a2a_app(request.scope, request.receive, request._send)
|
|
101
|
-
from starlette.responses import Response
|
|
102
|
-
|
|
103
111
|
return Response()
|
|
104
112
|
|
|
105
113
|
except Exception as e:
|
|
106
114
|
self.log.exception("Error handling A2A request", agent=agent_name)
|
|
107
|
-
return JSONResponse(
|
|
108
|
-
{"error": str(e)},
|
|
109
|
-
status_code=500,
|
|
110
|
-
)
|
|
115
|
+
return JSONResponse({"error": str(e)}, status_code=500)
|
|
111
116
|
|
|
112
117
|
# A2A protocol routes
|
|
113
118
|
routes.append(Route(f"/{agent_name}", agent_handler, methods=["POST"]))
|