agentex-sdk 0.1.0a6__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.
- agentex/__init__.py +103 -0
- agentex/_base_client.py +1992 -0
- agentex/_client.py +506 -0
- agentex/_compat.py +219 -0
- agentex/_constants.py +14 -0
- agentex/_exceptions.py +108 -0
- agentex/_files.py +123 -0
- agentex/_models.py +829 -0
- agentex/_qs.py +150 -0
- agentex/_resource.py +43 -0
- agentex/_response.py +830 -0
- agentex/_streaming.py +333 -0
- agentex/_types.py +219 -0
- agentex/_utils/__init__.py +57 -0
- agentex/_utils/_logs.py +25 -0
- agentex/_utils/_proxy.py +65 -0
- agentex/_utils/_reflection.py +42 -0
- agentex/_utils/_resources_proxy.py +24 -0
- agentex/_utils/_streams.py +12 -0
- agentex/_utils/_sync.py +86 -0
- agentex/_utils/_transform.py +447 -0
- agentex/_utils/_typing.py +151 -0
- agentex/_utils/_utils.py +422 -0
- agentex/_version.py +4 -0
- agentex/lib/.keep +4 -0
- agentex/lib/__init__.py +0 -0
- agentex/lib/adk/__init__.py +41 -0
- agentex/lib/adk/_modules/__init__.py +0 -0
- agentex/lib/adk/_modules/acp.py +247 -0
- agentex/lib/adk/_modules/agent_task_tracker.py +176 -0
- agentex/lib/adk/_modules/agents.py +77 -0
- agentex/lib/adk/_modules/events.py +141 -0
- agentex/lib/adk/_modules/messages.py +285 -0
- agentex/lib/adk/_modules/state.py +291 -0
- agentex/lib/adk/_modules/streaming.py +75 -0
- agentex/lib/adk/_modules/tasks.py +124 -0
- agentex/lib/adk/_modules/tracing.py +194 -0
- agentex/lib/adk/providers/__init__.py +9 -0
- agentex/lib/adk/providers/_modules/__init__.py +0 -0
- agentex/lib/adk/providers/_modules/litellm.py +232 -0
- agentex/lib/adk/providers/_modules/openai.py +416 -0
- agentex/lib/adk/providers/_modules/sgp.py +85 -0
- agentex/lib/adk/utils/__init__.py +5 -0
- agentex/lib/adk/utils/_modules/__init__.py +0 -0
- agentex/lib/adk/utils/_modules/templating.py +94 -0
- agentex/lib/cli/__init__.py +0 -0
- agentex/lib/cli/commands/__init__.py +0 -0
- agentex/lib/cli/commands/agents.py +328 -0
- agentex/lib/cli/commands/init.py +227 -0
- agentex/lib/cli/commands/main.py +33 -0
- agentex/lib/cli/commands/secrets.py +169 -0
- agentex/lib/cli/commands/tasks.py +118 -0
- agentex/lib/cli/commands/uv.py +133 -0
- agentex/lib/cli/handlers/__init__.py +0 -0
- agentex/lib/cli/handlers/agent_handlers.py +160 -0
- agentex/lib/cli/handlers/cleanup_handlers.py +186 -0
- agentex/lib/cli/handlers/deploy_handlers.py +351 -0
- agentex/lib/cli/handlers/run_handlers.py +452 -0
- agentex/lib/cli/handlers/secret_handlers.py +670 -0
- agentex/lib/cli/templates/default/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/default/Dockerfile-uv.j2 +42 -0
- agentex/lib/cli/templates/default/Dockerfile.j2 +42 -0
- agentex/lib/cli/templates/default/README.md.j2 +193 -0
- agentex/lib/cli/templates/default/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/default/manifest.yaml.j2 +116 -0
- agentex/lib/cli/templates/default/project/acp.py.j2 +29 -0
- agentex/lib/cli/templates/default/pyproject.toml.j2 +33 -0
- agentex/lib/cli/templates/default/requirements.txt.j2 +5 -0
- agentex/lib/cli/templates/deploy/Screenshot 2025-03-19 at 10.36.57/342/200/257AM.png +0 -0
- agentex/lib/cli/templates/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/sync/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/sync/Dockerfile-uv.j2 +42 -0
- agentex/lib/cli/templates/sync/Dockerfile.j2 +42 -0
- agentex/lib/cli/templates/sync/README.md.j2 +293 -0
- agentex/lib/cli/templates/sync/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/sync/manifest.yaml.j2 +116 -0
- agentex/lib/cli/templates/sync/project/acp.py.j2 +26 -0
- agentex/lib/cli/templates/sync/pyproject.toml.j2 +33 -0
- agentex/lib/cli/templates/sync/requirements.txt.j2 +5 -0
- agentex/lib/cli/templates/temporal/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/temporal/Dockerfile-uv.j2 +48 -0
- agentex/lib/cli/templates/temporal/Dockerfile.j2 +48 -0
- agentex/lib/cli/templates/temporal/README.md.j2 +316 -0
- agentex/lib/cli/templates/temporal/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/temporal/manifest.yaml.j2 +137 -0
- agentex/lib/cli/templates/temporal/project/acp.py.j2 +30 -0
- agentex/lib/cli/templates/temporal/project/run_worker.py.j2 +33 -0
- agentex/lib/cli/templates/temporal/project/workflow.py.j2 +66 -0
- agentex/lib/cli/templates/temporal/pyproject.toml.j2 +34 -0
- agentex/lib/cli/templates/temporal/requirements.txt.j2 +5 -0
- agentex/lib/cli/utils/cli_utils.py +14 -0
- agentex/lib/cli/utils/credential_utils.py +103 -0
- agentex/lib/cli/utils/exceptions.py +6 -0
- agentex/lib/cli/utils/kubectl_utils.py +135 -0
- agentex/lib/cli/utils/kubernetes_secrets_utils.py +185 -0
- agentex/lib/core/__init__.py +0 -0
- agentex/lib/core/adapters/__init__.py +0 -0
- agentex/lib/core/adapters/llm/__init__.py +1 -0
- agentex/lib/core/adapters/llm/adapter_litellm.py +46 -0
- agentex/lib/core/adapters/llm/adapter_sgp.py +55 -0
- agentex/lib/core/adapters/llm/port.py +24 -0
- agentex/lib/core/adapters/streams/adapter_redis.py +128 -0
- agentex/lib/core/adapters/streams/port.py +50 -0
- agentex/lib/core/clients/__init__.py +1 -0
- agentex/lib/core/clients/temporal/__init__.py +0 -0
- agentex/lib/core/clients/temporal/temporal_client.py +181 -0
- agentex/lib/core/clients/temporal/types.py +47 -0
- agentex/lib/core/clients/temporal/utils.py +56 -0
- agentex/lib/core/services/__init__.py +0 -0
- agentex/lib/core/services/adk/__init__.py +0 -0
- agentex/lib/core/services/adk/acp/__init__.py +0 -0
- agentex/lib/core/services/adk/acp/acp.py +210 -0
- agentex/lib/core/services/adk/agent_task_tracker.py +85 -0
- agentex/lib/core/services/adk/agents.py +43 -0
- agentex/lib/core/services/adk/events.py +61 -0
- agentex/lib/core/services/adk/messages.py +164 -0
- agentex/lib/core/services/adk/providers/__init__.py +0 -0
- agentex/lib/core/services/adk/providers/litellm.py +256 -0
- agentex/lib/core/services/adk/providers/openai.py +723 -0
- agentex/lib/core/services/adk/providers/sgp.py +99 -0
- agentex/lib/core/services/adk/state.py +120 -0
- agentex/lib/core/services/adk/streaming.py +262 -0
- agentex/lib/core/services/adk/tasks.py +69 -0
- agentex/lib/core/services/adk/tracing.py +36 -0
- agentex/lib/core/services/adk/utils/__init__.py +0 -0
- agentex/lib/core/services/adk/utils/templating.py +58 -0
- agentex/lib/core/temporal/__init__.py +0 -0
- agentex/lib/core/temporal/activities/__init__.py +207 -0
- agentex/lib/core/temporal/activities/activity_helpers.py +37 -0
- agentex/lib/core/temporal/activities/adk/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/acp/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/acp/acp_activities.py +86 -0
- agentex/lib/core/temporal/activities/adk/agent_task_tracker_activities.py +76 -0
- agentex/lib/core/temporal/activities/adk/agents_activities.py +35 -0
- agentex/lib/core/temporal/activities/adk/events_activities.py +50 -0
- agentex/lib/core/temporal/activities/adk/messages_activities.py +94 -0
- agentex/lib/core/temporal/activities/adk/providers/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/providers/litellm_activities.py +71 -0
- agentex/lib/core/temporal/activities/adk/providers/openai_activities.py +210 -0
- agentex/lib/core/temporal/activities/adk/providers/sgp_activities.py +42 -0
- agentex/lib/core/temporal/activities/adk/state_activities.py +85 -0
- agentex/lib/core/temporal/activities/adk/streaming_activities.py +33 -0
- agentex/lib/core/temporal/activities/adk/tasks_activities.py +48 -0
- agentex/lib/core/temporal/activities/adk/tracing_activities.py +55 -0
- agentex/lib/core/temporal/activities/adk/utils/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/utils/templating_activities.py +41 -0
- agentex/lib/core/temporal/services/__init__.py +0 -0
- agentex/lib/core/temporal/services/temporal_task_service.py +69 -0
- agentex/lib/core/temporal/types/__init__.py +0 -0
- agentex/lib/core/temporal/types/workflow.py +5 -0
- agentex/lib/core/temporal/workers/__init__.py +0 -0
- agentex/lib/core/temporal/workers/worker.py +162 -0
- agentex/lib/core/temporal/workflows/workflow.py +26 -0
- agentex/lib/core/tracing/__init__.py +5 -0
- agentex/lib/core/tracing/processors/agentex_tracing_processor.py +117 -0
- agentex/lib/core/tracing/processors/sgp_tracing_processor.py +119 -0
- agentex/lib/core/tracing/processors/tracing_processor_interface.py +40 -0
- agentex/lib/core/tracing/trace.py +311 -0
- agentex/lib/core/tracing/tracer.py +70 -0
- agentex/lib/core/tracing/tracing_processor_manager.py +62 -0
- agentex/lib/environment_variables.py +87 -0
- agentex/lib/py.typed +0 -0
- agentex/lib/sdk/__init__.py +0 -0
- agentex/lib/sdk/config/__init__.py +0 -0
- agentex/lib/sdk/config/agent_config.py +61 -0
- agentex/lib/sdk/config/agent_manifest.py +219 -0
- agentex/lib/sdk/config/build_config.py +35 -0
- agentex/lib/sdk/config/deployment_config.py +117 -0
- agentex/lib/sdk/config/local_development_config.py +56 -0
- agentex/lib/sdk/config/project_config.py +103 -0
- agentex/lib/sdk/fastacp/__init__.py +3 -0
- agentex/lib/sdk/fastacp/base/base_acp_server.py +406 -0
- agentex/lib/sdk/fastacp/fastacp.py +74 -0
- agentex/lib/sdk/fastacp/impl/agentic_base_acp.py +72 -0
- agentex/lib/sdk/fastacp/impl/sync_acp.py +109 -0
- agentex/lib/sdk/fastacp/impl/temporal_acp.py +97 -0
- agentex/lib/sdk/fastacp/tests/README.md +297 -0
- agentex/lib/sdk/fastacp/tests/conftest.py +307 -0
- agentex/lib/sdk/fastacp/tests/pytest.ini +10 -0
- agentex/lib/sdk/fastacp/tests/run_tests.py +227 -0
- agentex/lib/sdk/fastacp/tests/test_base_acp_server.py +450 -0
- agentex/lib/sdk/fastacp/tests/test_fastacp_factory.py +344 -0
- agentex/lib/sdk/fastacp/tests/test_integration.py +477 -0
- agentex/lib/sdk/state_machine/__init__.py +6 -0
- agentex/lib/sdk/state_machine/noop_workflow.py +21 -0
- agentex/lib/sdk/state_machine/state.py +10 -0
- agentex/lib/sdk/state_machine/state_machine.py +189 -0
- agentex/lib/sdk/state_machine/state_workflow.py +16 -0
- agentex/lib/sdk/utils/__init__.py +0 -0
- agentex/lib/sdk/utils/messages.py +223 -0
- agentex/lib/types/__init__.py +0 -0
- agentex/lib/types/acp.py +94 -0
- agentex/lib/types/agent_configs.py +79 -0
- agentex/lib/types/agent_results.py +29 -0
- agentex/lib/types/credentials.py +34 -0
- agentex/lib/types/fastacp.py +61 -0
- agentex/lib/types/files.py +13 -0
- agentex/lib/types/json_rpc.py +49 -0
- agentex/lib/types/llm_messages.py +354 -0
- agentex/lib/types/task_message_updates.py +171 -0
- agentex/lib/types/tracing.py +34 -0
- agentex/lib/utils/__init__.py +0 -0
- agentex/lib/utils/completions.py +131 -0
- agentex/lib/utils/console.py +14 -0
- agentex/lib/utils/io.py +29 -0
- agentex/lib/utils/iterables.py +14 -0
- agentex/lib/utils/json_schema.py +23 -0
- agentex/lib/utils/logging.py +31 -0
- agentex/lib/utils/mcp.py +17 -0
- agentex/lib/utils/model_utils.py +46 -0
- agentex/lib/utils/parsing.py +15 -0
- agentex/lib/utils/regex.py +6 -0
- agentex/lib/utils/temporal.py +13 -0
- agentex/py.typed +0 -0
- agentex/resources/__init__.py +103 -0
- agentex/resources/agents.py +707 -0
- agentex/resources/events.py +294 -0
- agentex/resources/messages/__init__.py +33 -0
- agentex/resources/messages/batch.py +271 -0
- agentex/resources/messages/messages.py +492 -0
- agentex/resources/spans.py +557 -0
- agentex/resources/states.py +544 -0
- agentex/resources/tasks.py +615 -0
- agentex/resources/tracker.py +384 -0
- agentex/types/__init__.py +56 -0
- agentex/types/acp_type.py +7 -0
- agentex/types/agent.py +29 -0
- agentex/types/agent_list_params.py +13 -0
- agentex/types/agent_list_response.py +10 -0
- agentex/types/agent_rpc_by_name_params.py +21 -0
- agentex/types/agent_rpc_params.py +51 -0
- agentex/types/agent_rpc_params1.py +21 -0
- agentex/types/agent_rpc_response.py +20 -0
- agentex/types/agent_rpc_result.py +90 -0
- agentex/types/agent_task_tracker.py +34 -0
- agentex/types/data_content.py +30 -0
- agentex/types/data_content_param.py +31 -0
- agentex/types/data_delta.py +14 -0
- agentex/types/event.py +29 -0
- agentex/types/event_list_params.py +22 -0
- agentex/types/event_list_response.py +10 -0
- agentex/types/message_author.py +7 -0
- agentex/types/message_create_params.py +18 -0
- agentex/types/message_list_params.py +14 -0
- agentex/types/message_list_response.py +10 -0
- agentex/types/message_style.py +7 -0
- agentex/types/message_update_params.py +18 -0
- agentex/types/messages/__init__.py +8 -0
- agentex/types/messages/batch_create_params.py +16 -0
- agentex/types/messages/batch_create_response.py +10 -0
- agentex/types/messages/batch_update_params.py +16 -0
- agentex/types/messages/batch_update_response.py +10 -0
- agentex/types/shared/__init__.py +3 -0
- agentex/types/shared/task_message_update.py +83 -0
- agentex/types/span.py +36 -0
- agentex/types/span_create_params.py +40 -0
- agentex/types/span_list_params.py +12 -0
- agentex/types/span_list_response.py +10 -0
- agentex/types/span_update_params.py +37 -0
- agentex/types/state.py +25 -0
- agentex/types/state_create_params.py +16 -0
- agentex/types/state_list_params.py +16 -0
- agentex/types/state_list_response.py +10 -0
- agentex/types/state_update_params.py +16 -0
- agentex/types/task.py +23 -0
- agentex/types/task_delete_by_name_response.py +8 -0
- agentex/types/task_delete_response.py +8 -0
- agentex/types/task_list_response.py +10 -0
- agentex/types/task_message.py +33 -0
- agentex/types/task_message_content.py +16 -0
- agentex/types/task_message_content_param.py +17 -0
- agentex/types/task_message_delta.py +16 -0
- agentex/types/text_content.py +53 -0
- agentex/types/text_content_param.py +54 -0
- agentex/types/text_delta.py +14 -0
- agentex/types/tool_request_content.py +36 -0
- agentex/types/tool_request_content_param.py +37 -0
- agentex/types/tool_request_delta.py +18 -0
- agentex/types/tool_response_content.py +36 -0
- agentex/types/tool_response_content_param.py +36 -0
- agentex/types/tool_response_delta.py +18 -0
- agentex/types/tracker_list_params.py +16 -0
- agentex/types/tracker_list_response.py +10 -0
- agentex/types/tracker_update_params.py +19 -0
- agentex_sdk-0.1.0a6.dist-info/METADATA +426 -0
- agentex_sdk-0.1.0a6.dist-info/RECORD +289 -0
- agentex_sdk-0.1.0a6.dist-info/WHEEL +4 -0
- agentex_sdk-0.1.0a6.dist-info/entry_points.txt +2 -0
- agentex_sdk-0.1.0a6.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,477 @@
|
|
1
|
+
import asyncio
|
2
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
3
|
+
|
4
|
+
import httpx
|
5
|
+
import pytest
|
6
|
+
|
7
|
+
from agentex.lib.sdk.fastacp.impl.agentic_base_acp import AgenticBaseACP
|
8
|
+
from agentex.lib.sdk.fastacp.impl.sync_acp import SyncACP
|
9
|
+
from agentex.lib.sdk.fastacp.impl.temporal_acp import TemporalACP
|
10
|
+
from agentex.lib.types.acp import (
|
11
|
+
CancelTaskParams,
|
12
|
+
CreateTaskParams,
|
13
|
+
RPCMethod,
|
14
|
+
SendEventParams,
|
15
|
+
)
|
16
|
+
|
17
|
+
|
18
|
+
class TestImplementationBehavior:
|
19
|
+
"""Test specific behavior differences between ACP implementations"""
|
20
|
+
|
21
|
+
@pytest.mark.asyncio()
|
22
|
+
async def test_sync_acp_default_handlers(self):
|
23
|
+
"""Test SyncACP has expected default handlers"""
|
24
|
+
with patch.dict("os.environ", {"AGENTEX_BASE_URL": ""}):
|
25
|
+
sync_acp = SyncACP.create()
|
26
|
+
|
27
|
+
# Should have send_message_message handler by default
|
28
|
+
assert RPCMethod.MESSAGE_SEND in sync_acp._handlers
|
29
|
+
|
30
|
+
@pytest.mark.asyncio()
|
31
|
+
async def test_agentic_acp_default_handlers(self):
|
32
|
+
"""Test AgenticBaseACP has expected default handlers"""
|
33
|
+
with patch.dict("os.environ", {"AGENTEX_BASE_URL": ""}):
|
34
|
+
agentic_acp = AgenticBaseACP.create()
|
35
|
+
|
36
|
+
# Should have create, message, and cancel handlers by default
|
37
|
+
assert RPCMethod.TASK_CREATE in agentic_acp._handlers
|
38
|
+
assert RPCMethod.EVENT_SEND in agentic_acp._handlers
|
39
|
+
assert RPCMethod.TASK_CANCEL in agentic_acp._handlers
|
40
|
+
|
41
|
+
@pytest.mark.asyncio()
|
42
|
+
async def test_temporal_acp_creation_with_mocked_client(self):
|
43
|
+
"""Test TemporalACP creation with mocked temporal client"""
|
44
|
+
with patch.dict("os.environ", {"AGENTEX_BASE_URL": ""}):
|
45
|
+
with patch.object(TemporalACP, "create", new_callable=AsyncMock) as mock_create:
|
46
|
+
mock_temporal_instance = MagicMock(spec=TemporalACP)
|
47
|
+
mock_temporal_instance._handlers = {}
|
48
|
+
mock_temporal_instance.temporal_client = MagicMock()
|
49
|
+
mock_create.return_value = mock_temporal_instance
|
50
|
+
|
51
|
+
temporal_acp = await TemporalACP.create()
|
52
|
+
|
53
|
+
assert temporal_acp == mock_temporal_instance
|
54
|
+
assert hasattr(temporal_acp, "temporal_client")
|
55
|
+
|
56
|
+
|
57
|
+
class TestRealWorldScenarios:
|
58
|
+
"""Test real-world usage scenarios and integration"""
|
59
|
+
|
60
|
+
@pytest.mark.asyncio()
|
61
|
+
async def test_message_handling_workflow(self, sync_acp, free_port, test_server_runner):
|
62
|
+
"""Test complete message handling workflow"""
|
63
|
+
messages_received = []
|
64
|
+
|
65
|
+
@sync_acp.on_task_event_send
|
66
|
+
async def message_handler(params: SendEventParams):
|
67
|
+
messages_received.append(
|
68
|
+
{
|
69
|
+
"task_id": params.task.id,
|
70
|
+
"message_content": params.message.content,
|
71
|
+
"author": params.message.author,
|
72
|
+
}
|
73
|
+
)
|
74
|
+
return {"processed": True}
|
75
|
+
|
76
|
+
runner = test_server_runner(sync_acp, free_port)
|
77
|
+
await runner.start()
|
78
|
+
|
79
|
+
# Send multiple messages
|
80
|
+
async with httpx.AsyncClient() as client:
|
81
|
+
for i in range(3):
|
82
|
+
request_data = {
|
83
|
+
"jsonrpc": "2.0",
|
84
|
+
"method": "event/send",
|
85
|
+
"params": {
|
86
|
+
"task": {
|
87
|
+
"id": f"workflow-task-{i}",
|
88
|
+
"agent_id": "workflow-agent",
|
89
|
+
"status": "RUNNING",
|
90
|
+
},
|
91
|
+
"message": {
|
92
|
+
"type": "text",
|
93
|
+
"author": "user",
|
94
|
+
"content": f"Workflow message {i}",
|
95
|
+
},
|
96
|
+
},
|
97
|
+
"id": f"workflow-{i}",
|
98
|
+
}
|
99
|
+
|
100
|
+
response = await client.post(f"http://127.0.0.1:{free_port}/api", json=request_data)
|
101
|
+
assert response.status_code == 200
|
102
|
+
|
103
|
+
# Give background tasks time to process
|
104
|
+
await asyncio.sleep(0.2)
|
105
|
+
|
106
|
+
# Verify all messages were processed
|
107
|
+
assert len(messages_received) == 3
|
108
|
+
for i, msg in enumerate(messages_received):
|
109
|
+
assert msg["task_id"] == f"workflow-task-{i}"
|
110
|
+
assert msg["message_content"] == f"Workflow message {i}"
|
111
|
+
assert msg["author"] == "user"
|
112
|
+
|
113
|
+
await runner.stop()
|
114
|
+
|
115
|
+
@pytest.mark.asyncio()
|
116
|
+
async def test_task_lifecycle_management(self, agentic_base_acp, free_port, test_server_runner):
|
117
|
+
"""Test complete task lifecycle: create -> message -> cancel"""
|
118
|
+
task_events = []
|
119
|
+
|
120
|
+
@agentic_base_acp.on_task_create
|
121
|
+
async def create_handler(params: CreateTaskParams):
|
122
|
+
task_events.append(("created", params.task.id))
|
123
|
+
|
124
|
+
@agentic_base_acp.on_task_event_send
|
125
|
+
async def message_handler(params: SendEventParams):
|
126
|
+
task_events.append(("message", params.task.id))
|
127
|
+
|
128
|
+
@agentic_base_acp.on_task_cancel
|
129
|
+
async def cancel_handler(params: CancelTaskParams):
|
130
|
+
task_events.append(("cancelled", params.task_id))
|
131
|
+
|
132
|
+
runner = test_server_runner(agentic_base_acp, free_port)
|
133
|
+
await runner.start()
|
134
|
+
|
135
|
+
async with httpx.AsyncClient() as client:
|
136
|
+
# Create task
|
137
|
+
create_request = {
|
138
|
+
"jsonrpc": "2.0",
|
139
|
+
"method": "task/create",
|
140
|
+
"params": {
|
141
|
+
"task": {
|
142
|
+
"id": "lifecycle-task",
|
143
|
+
"agent_id": "lifecycle-agent",
|
144
|
+
"status": "RUNNING",
|
145
|
+
}
|
146
|
+
},
|
147
|
+
"id": "create-1",
|
148
|
+
}
|
149
|
+
|
150
|
+
response = await client.post(f"http://127.0.0.1:{free_port}/api", json=create_request)
|
151
|
+
assert response.status_code == 200
|
152
|
+
|
153
|
+
# Send message
|
154
|
+
message_request = {
|
155
|
+
"jsonrpc": "2.0",
|
156
|
+
"method": "event/send",
|
157
|
+
"params": {
|
158
|
+
"task": {
|
159
|
+
"id": "lifecycle-task",
|
160
|
+
"agent_id": "lifecycle-agent",
|
161
|
+
"status": "RUNNING",
|
162
|
+
},
|
163
|
+
"message": {
|
164
|
+
"type": "text",
|
165
|
+
"author": "user",
|
166
|
+
"content": "Lifecycle test message",
|
167
|
+
},
|
168
|
+
},
|
169
|
+
"id": "message-1",
|
170
|
+
}
|
171
|
+
|
172
|
+
response = await client.post(f"http://127.0.0.1:{free_port}/api", json=message_request)
|
173
|
+
assert response.status_code == 200
|
174
|
+
|
175
|
+
# Cancel task
|
176
|
+
cancel_request = {
|
177
|
+
"jsonrpc": "2.0",
|
178
|
+
"method": "task/cancel",
|
179
|
+
"params": {"task_id": "lifecycle-task"},
|
180
|
+
"id": "cancel-1",
|
181
|
+
}
|
182
|
+
|
183
|
+
response = await client.post(f"http://127.0.0.1:{free_port}/api", json=cancel_request)
|
184
|
+
assert response.status_code == 200
|
185
|
+
|
186
|
+
# Give background tasks time to process
|
187
|
+
await asyncio.sleep(0.2)
|
188
|
+
|
189
|
+
# Verify task lifecycle events
|
190
|
+
assert len(task_events) == 3
|
191
|
+
assert task_events[0] == ("created", "lifecycle-task")
|
192
|
+
assert task_events[1] == ("message", "lifecycle-task")
|
193
|
+
assert task_events[2] == ("cancelled", "lifecycle-task")
|
194
|
+
|
195
|
+
await runner.stop()
|
196
|
+
|
197
|
+
|
198
|
+
class TestErrorRecovery:
|
199
|
+
"""Test error handling and recovery scenarios"""
|
200
|
+
|
201
|
+
@pytest.mark.asyncio()
|
202
|
+
async def test_server_resilience_to_handler_failures(
|
203
|
+
self, sync_acp, free_port, test_server_runner
|
204
|
+
):
|
205
|
+
"""Test server continues working after handler failures"""
|
206
|
+
failure_count = 0
|
207
|
+
success_count = 0
|
208
|
+
|
209
|
+
@sync_acp.on_task_event_send
|
210
|
+
async def unreliable_handler(params: SendEventParams):
|
211
|
+
nonlocal failure_count, success_count
|
212
|
+
if "fail" in params.message.content:
|
213
|
+
failure_count += 1
|
214
|
+
raise RuntimeError("Simulated handler failure")
|
215
|
+
else:
|
216
|
+
success_count += 1
|
217
|
+
return {"success": True}
|
218
|
+
|
219
|
+
runner = test_server_runner(sync_acp, free_port)
|
220
|
+
await runner.start()
|
221
|
+
|
222
|
+
async with httpx.AsyncClient() as client:
|
223
|
+
# Send failing request
|
224
|
+
fail_request = {
|
225
|
+
"jsonrpc": "2.0",
|
226
|
+
"method": "event/send",
|
227
|
+
"params": {
|
228
|
+
"task": {"id": "fail-task", "agent_id": "test-agent", "status": "RUNNING"},
|
229
|
+
"message": {"type": "text", "author": "user", "content": "This should fail"},
|
230
|
+
},
|
231
|
+
"id": "fail-1",
|
232
|
+
}
|
233
|
+
|
234
|
+
response = await client.post(f"http://127.0.0.1:{free_port}/api", json=fail_request)
|
235
|
+
assert response.status_code == 200 # Server should still respond
|
236
|
+
|
237
|
+
# Send successful request after failure
|
238
|
+
success_request = {
|
239
|
+
"jsonrpc": "2.0",
|
240
|
+
"method": "event/send",
|
241
|
+
"params": {
|
242
|
+
"task": {"id": "success-task", "agent_id": "test-agent", "status": "RUNNING"},
|
243
|
+
"message": {"type": "text", "author": "user", "content": "This should succeed"},
|
244
|
+
},
|
245
|
+
"id": "success-1",
|
246
|
+
}
|
247
|
+
|
248
|
+
response = await client.post(f"http://127.0.0.1:{free_port}/api", json=success_request)
|
249
|
+
assert response.status_code == 200
|
250
|
+
|
251
|
+
# Verify server is still healthy
|
252
|
+
health_response = await client.get(f"http://127.0.0.1:{free_port}/healthz")
|
253
|
+
assert health_response.status_code == 200
|
254
|
+
|
255
|
+
# Give background tasks time to process
|
256
|
+
await asyncio.sleep(0.2)
|
257
|
+
|
258
|
+
assert failure_count == 1
|
259
|
+
assert success_count == 1
|
260
|
+
|
261
|
+
await runner.stop()
|
262
|
+
|
263
|
+
@pytest.mark.asyncio()
|
264
|
+
async def test_concurrent_request_handling(self, sync_acp, free_port, test_server_runner):
|
265
|
+
"""Test handling multiple concurrent requests"""
|
266
|
+
processed_requests = []
|
267
|
+
|
268
|
+
@sync_acp.on_task_event_send
|
269
|
+
async def concurrent_handler(params: SendEventParams):
|
270
|
+
# Simulate some processing time
|
271
|
+
await asyncio.sleep(0.05)
|
272
|
+
processed_requests.append(params.task.id)
|
273
|
+
return {"processed": params.task.id}
|
274
|
+
|
275
|
+
runner = test_server_runner(sync_acp, free_port)
|
276
|
+
await runner.start()
|
277
|
+
|
278
|
+
# Send multiple concurrent requests
|
279
|
+
async def send_request(client, task_id):
|
280
|
+
request_data = {
|
281
|
+
"jsonrpc": "2.0",
|
282
|
+
"method": "event/send",
|
283
|
+
"params": {
|
284
|
+
"task": {"id": task_id, "agent_id": "concurrent-agent", "status": "RUNNING"},
|
285
|
+
"message": {
|
286
|
+
"type": "text",
|
287
|
+
"author": "user",
|
288
|
+
"content": f"Concurrent message for {task_id}",
|
289
|
+
},
|
290
|
+
},
|
291
|
+
"id": f"concurrent-{task_id}",
|
292
|
+
}
|
293
|
+
|
294
|
+
return await client.post(f"http://127.0.0.1:{free_port}/api", json=request_data)
|
295
|
+
|
296
|
+
async with httpx.AsyncClient() as client:
|
297
|
+
# Send 5 concurrent requests
|
298
|
+
tasks = [send_request(client, f"task-{i}") for i in range(5)]
|
299
|
+
responses = await asyncio.gather(*tasks)
|
300
|
+
|
301
|
+
# All should return immediate acknowledgment
|
302
|
+
for response in responses:
|
303
|
+
assert response.status_code == 200
|
304
|
+
data = response.json()
|
305
|
+
assert data["result"]["status"] == "processing"
|
306
|
+
|
307
|
+
# Give background tasks time to complete
|
308
|
+
await asyncio.sleep(0.3)
|
309
|
+
|
310
|
+
# All requests should have been processed
|
311
|
+
assert len(processed_requests) == 5
|
312
|
+
assert set(processed_requests) == {f"task-{i}" for i in range(5)}
|
313
|
+
|
314
|
+
await runner.stop()
|
315
|
+
|
316
|
+
|
317
|
+
class TestSpecialCases:
|
318
|
+
"""Test edge cases and special scenarios"""
|
319
|
+
|
320
|
+
@pytest.mark.asyncio()
|
321
|
+
async def test_notification_vs_request_behavior(self, sync_acp, free_port, test_server_runner):
|
322
|
+
"""Test difference between notifications (no ID) and requests (with ID)"""
|
323
|
+
notifications_received = 0
|
324
|
+
requests_received = 0
|
325
|
+
|
326
|
+
@sync_acp.on_task_event_send
|
327
|
+
async def tracking_handler(params: SendEventParams):
|
328
|
+
nonlocal notifications_received, requests_received
|
329
|
+
if "notification" in params.message.content:
|
330
|
+
notifications_received += 1
|
331
|
+
else:
|
332
|
+
requests_received += 1
|
333
|
+
return {"handled": True}
|
334
|
+
|
335
|
+
runner = test_server_runner(sync_acp, free_port)
|
336
|
+
await runner.start()
|
337
|
+
|
338
|
+
async with httpx.AsyncClient() as client:
|
339
|
+
# Send notification (no ID)
|
340
|
+
notification_data = {
|
341
|
+
"jsonrpc": "2.0",
|
342
|
+
"method": "event/send",
|
343
|
+
"params": {
|
344
|
+
"task": {
|
345
|
+
"id": "notification-task",
|
346
|
+
"agent_id": "test-agent",
|
347
|
+
"status": "RUNNING",
|
348
|
+
},
|
349
|
+
"message": {
|
350
|
+
"type": "text",
|
351
|
+
"author": "user",
|
352
|
+
"content": "This is a notification",
|
353
|
+
},
|
354
|
+
},
|
355
|
+
# Note: no "id" field
|
356
|
+
}
|
357
|
+
|
358
|
+
notification_response = await client.post(
|
359
|
+
f"http://127.0.0.1:{free_port}/api", json=notification_data
|
360
|
+
)
|
361
|
+
assert notification_response.status_code == 200
|
362
|
+
notification_result = notification_response.json()
|
363
|
+
assert notification_result["id"] is None
|
364
|
+
|
365
|
+
# Send regular request (with ID)
|
366
|
+
request_data = {
|
367
|
+
"jsonrpc": "2.0",
|
368
|
+
"method": "event/send",
|
369
|
+
"params": {
|
370
|
+
"task": {"id": "request-task", "agent_id": "test-agent", "status": "RUNNING"},
|
371
|
+
"message": {"type": "text", "author": "user", "content": "This is a request"},
|
372
|
+
},
|
373
|
+
"id": "request-1",
|
374
|
+
}
|
375
|
+
|
376
|
+
request_response = await client.post(
|
377
|
+
f"http://127.0.0.1:{free_port}/api", json=request_data
|
378
|
+
)
|
379
|
+
assert request_response.status_code == 200
|
380
|
+
request_result = request_response.json()
|
381
|
+
assert request_result["id"] == "request-1"
|
382
|
+
assert request_result["result"]["status"] == "processing"
|
383
|
+
|
384
|
+
# Give background tasks time to process
|
385
|
+
await asyncio.sleep(0.1)
|
386
|
+
|
387
|
+
assert notifications_received == 1
|
388
|
+
assert requests_received == 1
|
389
|
+
|
390
|
+
await runner.stop()
|
391
|
+
|
392
|
+
@pytest.mark.asyncio()
|
393
|
+
async def test_unicode_message_handling(self, sync_acp, free_port, test_server_runner):
|
394
|
+
"""Test handling of unicode characters in messages"""
|
395
|
+
received_message = None
|
396
|
+
|
397
|
+
@sync_acp.on_task_event_send
|
398
|
+
async def unicode_handler(params: SendEventParams):
|
399
|
+
nonlocal received_message
|
400
|
+
received_message = params.message.content
|
401
|
+
return {"unicode_handled": True}
|
402
|
+
|
403
|
+
runner = test_server_runner(sync_acp, free_port)
|
404
|
+
await runner.start()
|
405
|
+
|
406
|
+
unicode_text = "Hello 世界 🌍 émojis 🚀 and special chars: \n\t\r"
|
407
|
+
|
408
|
+
async with httpx.AsyncClient() as client:
|
409
|
+
request_data = {
|
410
|
+
"jsonrpc": "2.0",
|
411
|
+
"method": "event/send",
|
412
|
+
"params": {
|
413
|
+
"task": {
|
414
|
+
"id": "unicode-task",
|
415
|
+
"agent_id": "unicode-agent",
|
416
|
+
"status": "RUNNING",
|
417
|
+
},
|
418
|
+
"message": {"type": "text", "author": "user", "content": unicode_text},
|
419
|
+
},
|
420
|
+
"id": "unicode-test",
|
421
|
+
}
|
422
|
+
|
423
|
+
response = await client.post(f"http://127.0.0.1:{free_port}/api", json=request_data)
|
424
|
+
|
425
|
+
assert response.status_code == 200
|
426
|
+
|
427
|
+
# Give background task time to process
|
428
|
+
await asyncio.sleep(0.1)
|
429
|
+
|
430
|
+
assert received_message == unicode_text
|
431
|
+
|
432
|
+
await runner.stop()
|
433
|
+
|
434
|
+
|
435
|
+
class TestImplementationIsolation:
|
436
|
+
"""Test that different implementations don't interfere with each other"""
|
437
|
+
|
438
|
+
@pytest.mark.asyncio()
|
439
|
+
async def test_handler_isolation_between_implementations(self):
|
440
|
+
"""Test handlers registered on one implementation don't affect others"""
|
441
|
+
with patch.dict("os.environ", {"AGENTEX_BASE_URL": ""}):
|
442
|
+
sync_acp = SyncACP.create()
|
443
|
+
agentic_acp = AgenticBaseACP.create()
|
444
|
+
|
445
|
+
sync_handled = False
|
446
|
+
agentic_handled = False
|
447
|
+
|
448
|
+
@sync_acp.on_task_event_send
|
449
|
+
async def sync_handler(params: SendEventParams):
|
450
|
+
nonlocal sync_handled
|
451
|
+
sync_handled = True
|
452
|
+
return {"sync": True}
|
453
|
+
|
454
|
+
@agentic_acp.on_task_event_send
|
455
|
+
async def agentic_handler(params: SendEventParams):
|
456
|
+
nonlocal agentic_handled
|
457
|
+
agentic_handled = True
|
458
|
+
return {"agentic": True}
|
459
|
+
|
460
|
+
# Create test parameters
|
461
|
+
message_params = SendEventParams(
|
462
|
+
task={"id": "isolation-test-task", "agent_id": "test-agent", "status": "RUNNING"},
|
463
|
+
message={"type": "text", "author": "user", "content": "Isolation test"},
|
464
|
+
)
|
465
|
+
|
466
|
+
# Execute sync handler
|
467
|
+
sync_result = await sync_acp._handlers[RPCMethod.EVENT_SEND](message_params)
|
468
|
+
assert sync_handled is True
|
469
|
+
assert agentic_handled is False
|
470
|
+
assert sync_result == {"sync": True}
|
471
|
+
|
472
|
+
# Reset and execute agentic handler
|
473
|
+
sync_handled = False
|
474
|
+
agentic_result = await agentic_acp._handlers[RPCMethod.EVENT_SEND](message_params)
|
475
|
+
assert sync_handled is False
|
476
|
+
assert agentic_handled is True
|
477
|
+
assert agentic_result == {"agentic": True}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from pydantic import BaseModel
|
2
|
+
|
3
|
+
from agentex.lib.sdk.state_machine.state_workflow import StateWorkflow
|
4
|
+
from agentex.lib.utils.logging import make_logger
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from agentex.lib.sdk.state_machine import StateMachine
|
9
|
+
|
10
|
+
logger = make_logger(__name__)
|
11
|
+
|
12
|
+
|
13
|
+
class NoOpWorkflow(StateWorkflow):
|
14
|
+
"""
|
15
|
+
Workflow that does nothing. This is commonly used as a terminal state.
|
16
|
+
"""
|
17
|
+
|
18
|
+
async def execute(
|
19
|
+
self, state_machine: "StateMachine", state_machine_data: BaseModel | None = None
|
20
|
+
) -> str:
|
21
|
+
pass
|