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.
Files changed (289) hide show
  1. agentex/__init__.py +103 -0
  2. agentex/_base_client.py +1992 -0
  3. agentex/_client.py +506 -0
  4. agentex/_compat.py +219 -0
  5. agentex/_constants.py +14 -0
  6. agentex/_exceptions.py +108 -0
  7. agentex/_files.py +123 -0
  8. agentex/_models.py +829 -0
  9. agentex/_qs.py +150 -0
  10. agentex/_resource.py +43 -0
  11. agentex/_response.py +830 -0
  12. agentex/_streaming.py +333 -0
  13. agentex/_types.py +219 -0
  14. agentex/_utils/__init__.py +57 -0
  15. agentex/_utils/_logs.py +25 -0
  16. agentex/_utils/_proxy.py +65 -0
  17. agentex/_utils/_reflection.py +42 -0
  18. agentex/_utils/_resources_proxy.py +24 -0
  19. agentex/_utils/_streams.py +12 -0
  20. agentex/_utils/_sync.py +86 -0
  21. agentex/_utils/_transform.py +447 -0
  22. agentex/_utils/_typing.py +151 -0
  23. agentex/_utils/_utils.py +422 -0
  24. agentex/_version.py +4 -0
  25. agentex/lib/.keep +4 -0
  26. agentex/lib/__init__.py +0 -0
  27. agentex/lib/adk/__init__.py +41 -0
  28. agentex/lib/adk/_modules/__init__.py +0 -0
  29. agentex/lib/adk/_modules/acp.py +247 -0
  30. agentex/lib/adk/_modules/agent_task_tracker.py +176 -0
  31. agentex/lib/adk/_modules/agents.py +77 -0
  32. agentex/lib/adk/_modules/events.py +141 -0
  33. agentex/lib/adk/_modules/messages.py +285 -0
  34. agentex/lib/adk/_modules/state.py +291 -0
  35. agentex/lib/adk/_modules/streaming.py +75 -0
  36. agentex/lib/adk/_modules/tasks.py +124 -0
  37. agentex/lib/adk/_modules/tracing.py +194 -0
  38. agentex/lib/adk/providers/__init__.py +9 -0
  39. agentex/lib/adk/providers/_modules/__init__.py +0 -0
  40. agentex/lib/adk/providers/_modules/litellm.py +232 -0
  41. agentex/lib/adk/providers/_modules/openai.py +416 -0
  42. agentex/lib/adk/providers/_modules/sgp.py +85 -0
  43. agentex/lib/adk/utils/__init__.py +5 -0
  44. agentex/lib/adk/utils/_modules/__init__.py +0 -0
  45. agentex/lib/adk/utils/_modules/templating.py +94 -0
  46. agentex/lib/cli/__init__.py +0 -0
  47. agentex/lib/cli/commands/__init__.py +0 -0
  48. agentex/lib/cli/commands/agents.py +328 -0
  49. agentex/lib/cli/commands/init.py +227 -0
  50. agentex/lib/cli/commands/main.py +33 -0
  51. agentex/lib/cli/commands/secrets.py +169 -0
  52. agentex/lib/cli/commands/tasks.py +118 -0
  53. agentex/lib/cli/commands/uv.py +133 -0
  54. agentex/lib/cli/handlers/__init__.py +0 -0
  55. agentex/lib/cli/handlers/agent_handlers.py +160 -0
  56. agentex/lib/cli/handlers/cleanup_handlers.py +186 -0
  57. agentex/lib/cli/handlers/deploy_handlers.py +351 -0
  58. agentex/lib/cli/handlers/run_handlers.py +452 -0
  59. agentex/lib/cli/handlers/secret_handlers.py +670 -0
  60. agentex/lib/cli/templates/default/.dockerignore.j2 +43 -0
  61. agentex/lib/cli/templates/default/Dockerfile-uv.j2 +42 -0
  62. agentex/lib/cli/templates/default/Dockerfile.j2 +42 -0
  63. agentex/lib/cli/templates/default/README.md.j2 +193 -0
  64. agentex/lib/cli/templates/default/deploy/example.yaml.j2 +55 -0
  65. agentex/lib/cli/templates/default/manifest.yaml.j2 +116 -0
  66. agentex/lib/cli/templates/default/project/acp.py.j2 +29 -0
  67. agentex/lib/cli/templates/default/pyproject.toml.j2 +33 -0
  68. agentex/lib/cli/templates/default/requirements.txt.j2 +5 -0
  69. agentex/lib/cli/templates/deploy/Screenshot 2025-03-19 at 10.36.57/342/200/257AM.png +0 -0
  70. agentex/lib/cli/templates/deploy/example.yaml.j2 +55 -0
  71. agentex/lib/cli/templates/sync/.dockerignore.j2 +43 -0
  72. agentex/lib/cli/templates/sync/Dockerfile-uv.j2 +42 -0
  73. agentex/lib/cli/templates/sync/Dockerfile.j2 +42 -0
  74. agentex/lib/cli/templates/sync/README.md.j2 +293 -0
  75. agentex/lib/cli/templates/sync/deploy/example.yaml.j2 +55 -0
  76. agentex/lib/cli/templates/sync/manifest.yaml.j2 +116 -0
  77. agentex/lib/cli/templates/sync/project/acp.py.j2 +26 -0
  78. agentex/lib/cli/templates/sync/pyproject.toml.j2 +33 -0
  79. agentex/lib/cli/templates/sync/requirements.txt.j2 +5 -0
  80. agentex/lib/cli/templates/temporal/.dockerignore.j2 +43 -0
  81. agentex/lib/cli/templates/temporal/Dockerfile-uv.j2 +48 -0
  82. agentex/lib/cli/templates/temporal/Dockerfile.j2 +48 -0
  83. agentex/lib/cli/templates/temporal/README.md.j2 +316 -0
  84. agentex/lib/cli/templates/temporal/deploy/example.yaml.j2 +55 -0
  85. agentex/lib/cli/templates/temporal/manifest.yaml.j2 +137 -0
  86. agentex/lib/cli/templates/temporal/project/acp.py.j2 +30 -0
  87. agentex/lib/cli/templates/temporal/project/run_worker.py.j2 +33 -0
  88. agentex/lib/cli/templates/temporal/project/workflow.py.j2 +66 -0
  89. agentex/lib/cli/templates/temporal/pyproject.toml.j2 +34 -0
  90. agentex/lib/cli/templates/temporal/requirements.txt.j2 +5 -0
  91. agentex/lib/cli/utils/cli_utils.py +14 -0
  92. agentex/lib/cli/utils/credential_utils.py +103 -0
  93. agentex/lib/cli/utils/exceptions.py +6 -0
  94. agentex/lib/cli/utils/kubectl_utils.py +135 -0
  95. agentex/lib/cli/utils/kubernetes_secrets_utils.py +185 -0
  96. agentex/lib/core/__init__.py +0 -0
  97. agentex/lib/core/adapters/__init__.py +0 -0
  98. agentex/lib/core/adapters/llm/__init__.py +1 -0
  99. agentex/lib/core/adapters/llm/adapter_litellm.py +46 -0
  100. agentex/lib/core/adapters/llm/adapter_sgp.py +55 -0
  101. agentex/lib/core/adapters/llm/port.py +24 -0
  102. agentex/lib/core/adapters/streams/adapter_redis.py +128 -0
  103. agentex/lib/core/adapters/streams/port.py +50 -0
  104. agentex/lib/core/clients/__init__.py +1 -0
  105. agentex/lib/core/clients/temporal/__init__.py +0 -0
  106. agentex/lib/core/clients/temporal/temporal_client.py +181 -0
  107. agentex/lib/core/clients/temporal/types.py +47 -0
  108. agentex/lib/core/clients/temporal/utils.py +56 -0
  109. agentex/lib/core/services/__init__.py +0 -0
  110. agentex/lib/core/services/adk/__init__.py +0 -0
  111. agentex/lib/core/services/adk/acp/__init__.py +0 -0
  112. agentex/lib/core/services/adk/acp/acp.py +210 -0
  113. agentex/lib/core/services/adk/agent_task_tracker.py +85 -0
  114. agentex/lib/core/services/adk/agents.py +43 -0
  115. agentex/lib/core/services/adk/events.py +61 -0
  116. agentex/lib/core/services/adk/messages.py +164 -0
  117. agentex/lib/core/services/adk/providers/__init__.py +0 -0
  118. agentex/lib/core/services/adk/providers/litellm.py +256 -0
  119. agentex/lib/core/services/adk/providers/openai.py +723 -0
  120. agentex/lib/core/services/adk/providers/sgp.py +99 -0
  121. agentex/lib/core/services/adk/state.py +120 -0
  122. agentex/lib/core/services/adk/streaming.py +262 -0
  123. agentex/lib/core/services/adk/tasks.py +69 -0
  124. agentex/lib/core/services/adk/tracing.py +36 -0
  125. agentex/lib/core/services/adk/utils/__init__.py +0 -0
  126. agentex/lib/core/services/adk/utils/templating.py +58 -0
  127. agentex/lib/core/temporal/__init__.py +0 -0
  128. agentex/lib/core/temporal/activities/__init__.py +207 -0
  129. agentex/lib/core/temporal/activities/activity_helpers.py +37 -0
  130. agentex/lib/core/temporal/activities/adk/__init__.py +0 -0
  131. agentex/lib/core/temporal/activities/adk/acp/__init__.py +0 -0
  132. agentex/lib/core/temporal/activities/adk/acp/acp_activities.py +86 -0
  133. agentex/lib/core/temporal/activities/adk/agent_task_tracker_activities.py +76 -0
  134. agentex/lib/core/temporal/activities/adk/agents_activities.py +35 -0
  135. agentex/lib/core/temporal/activities/adk/events_activities.py +50 -0
  136. agentex/lib/core/temporal/activities/adk/messages_activities.py +94 -0
  137. agentex/lib/core/temporal/activities/adk/providers/__init__.py +0 -0
  138. agentex/lib/core/temporal/activities/adk/providers/litellm_activities.py +71 -0
  139. agentex/lib/core/temporal/activities/adk/providers/openai_activities.py +210 -0
  140. agentex/lib/core/temporal/activities/adk/providers/sgp_activities.py +42 -0
  141. agentex/lib/core/temporal/activities/adk/state_activities.py +85 -0
  142. agentex/lib/core/temporal/activities/adk/streaming_activities.py +33 -0
  143. agentex/lib/core/temporal/activities/adk/tasks_activities.py +48 -0
  144. agentex/lib/core/temporal/activities/adk/tracing_activities.py +55 -0
  145. agentex/lib/core/temporal/activities/adk/utils/__init__.py +0 -0
  146. agentex/lib/core/temporal/activities/adk/utils/templating_activities.py +41 -0
  147. agentex/lib/core/temporal/services/__init__.py +0 -0
  148. agentex/lib/core/temporal/services/temporal_task_service.py +69 -0
  149. agentex/lib/core/temporal/types/__init__.py +0 -0
  150. agentex/lib/core/temporal/types/workflow.py +5 -0
  151. agentex/lib/core/temporal/workers/__init__.py +0 -0
  152. agentex/lib/core/temporal/workers/worker.py +162 -0
  153. agentex/lib/core/temporal/workflows/workflow.py +26 -0
  154. agentex/lib/core/tracing/__init__.py +5 -0
  155. agentex/lib/core/tracing/processors/agentex_tracing_processor.py +117 -0
  156. agentex/lib/core/tracing/processors/sgp_tracing_processor.py +119 -0
  157. agentex/lib/core/tracing/processors/tracing_processor_interface.py +40 -0
  158. agentex/lib/core/tracing/trace.py +311 -0
  159. agentex/lib/core/tracing/tracer.py +70 -0
  160. agentex/lib/core/tracing/tracing_processor_manager.py +62 -0
  161. agentex/lib/environment_variables.py +87 -0
  162. agentex/lib/py.typed +0 -0
  163. agentex/lib/sdk/__init__.py +0 -0
  164. agentex/lib/sdk/config/__init__.py +0 -0
  165. agentex/lib/sdk/config/agent_config.py +61 -0
  166. agentex/lib/sdk/config/agent_manifest.py +219 -0
  167. agentex/lib/sdk/config/build_config.py +35 -0
  168. agentex/lib/sdk/config/deployment_config.py +117 -0
  169. agentex/lib/sdk/config/local_development_config.py +56 -0
  170. agentex/lib/sdk/config/project_config.py +103 -0
  171. agentex/lib/sdk/fastacp/__init__.py +3 -0
  172. agentex/lib/sdk/fastacp/base/base_acp_server.py +406 -0
  173. agentex/lib/sdk/fastacp/fastacp.py +74 -0
  174. agentex/lib/sdk/fastacp/impl/agentic_base_acp.py +72 -0
  175. agentex/lib/sdk/fastacp/impl/sync_acp.py +109 -0
  176. agentex/lib/sdk/fastacp/impl/temporal_acp.py +97 -0
  177. agentex/lib/sdk/fastacp/tests/README.md +297 -0
  178. agentex/lib/sdk/fastacp/tests/conftest.py +307 -0
  179. agentex/lib/sdk/fastacp/tests/pytest.ini +10 -0
  180. agentex/lib/sdk/fastacp/tests/run_tests.py +227 -0
  181. agentex/lib/sdk/fastacp/tests/test_base_acp_server.py +450 -0
  182. agentex/lib/sdk/fastacp/tests/test_fastacp_factory.py +344 -0
  183. agentex/lib/sdk/fastacp/tests/test_integration.py +477 -0
  184. agentex/lib/sdk/state_machine/__init__.py +6 -0
  185. agentex/lib/sdk/state_machine/noop_workflow.py +21 -0
  186. agentex/lib/sdk/state_machine/state.py +10 -0
  187. agentex/lib/sdk/state_machine/state_machine.py +189 -0
  188. agentex/lib/sdk/state_machine/state_workflow.py +16 -0
  189. agentex/lib/sdk/utils/__init__.py +0 -0
  190. agentex/lib/sdk/utils/messages.py +223 -0
  191. agentex/lib/types/__init__.py +0 -0
  192. agentex/lib/types/acp.py +94 -0
  193. agentex/lib/types/agent_configs.py +79 -0
  194. agentex/lib/types/agent_results.py +29 -0
  195. agentex/lib/types/credentials.py +34 -0
  196. agentex/lib/types/fastacp.py +61 -0
  197. agentex/lib/types/files.py +13 -0
  198. agentex/lib/types/json_rpc.py +49 -0
  199. agentex/lib/types/llm_messages.py +354 -0
  200. agentex/lib/types/task_message_updates.py +171 -0
  201. agentex/lib/types/tracing.py +34 -0
  202. agentex/lib/utils/__init__.py +0 -0
  203. agentex/lib/utils/completions.py +131 -0
  204. agentex/lib/utils/console.py +14 -0
  205. agentex/lib/utils/io.py +29 -0
  206. agentex/lib/utils/iterables.py +14 -0
  207. agentex/lib/utils/json_schema.py +23 -0
  208. agentex/lib/utils/logging.py +31 -0
  209. agentex/lib/utils/mcp.py +17 -0
  210. agentex/lib/utils/model_utils.py +46 -0
  211. agentex/lib/utils/parsing.py +15 -0
  212. agentex/lib/utils/regex.py +6 -0
  213. agentex/lib/utils/temporal.py +13 -0
  214. agentex/py.typed +0 -0
  215. agentex/resources/__init__.py +103 -0
  216. agentex/resources/agents.py +707 -0
  217. agentex/resources/events.py +294 -0
  218. agentex/resources/messages/__init__.py +33 -0
  219. agentex/resources/messages/batch.py +271 -0
  220. agentex/resources/messages/messages.py +492 -0
  221. agentex/resources/spans.py +557 -0
  222. agentex/resources/states.py +544 -0
  223. agentex/resources/tasks.py +615 -0
  224. agentex/resources/tracker.py +384 -0
  225. agentex/types/__init__.py +56 -0
  226. agentex/types/acp_type.py +7 -0
  227. agentex/types/agent.py +29 -0
  228. agentex/types/agent_list_params.py +13 -0
  229. agentex/types/agent_list_response.py +10 -0
  230. agentex/types/agent_rpc_by_name_params.py +21 -0
  231. agentex/types/agent_rpc_params.py +51 -0
  232. agentex/types/agent_rpc_params1.py +21 -0
  233. agentex/types/agent_rpc_response.py +20 -0
  234. agentex/types/agent_rpc_result.py +90 -0
  235. agentex/types/agent_task_tracker.py +34 -0
  236. agentex/types/data_content.py +30 -0
  237. agentex/types/data_content_param.py +31 -0
  238. agentex/types/data_delta.py +14 -0
  239. agentex/types/event.py +29 -0
  240. agentex/types/event_list_params.py +22 -0
  241. agentex/types/event_list_response.py +10 -0
  242. agentex/types/message_author.py +7 -0
  243. agentex/types/message_create_params.py +18 -0
  244. agentex/types/message_list_params.py +14 -0
  245. agentex/types/message_list_response.py +10 -0
  246. agentex/types/message_style.py +7 -0
  247. agentex/types/message_update_params.py +18 -0
  248. agentex/types/messages/__init__.py +8 -0
  249. agentex/types/messages/batch_create_params.py +16 -0
  250. agentex/types/messages/batch_create_response.py +10 -0
  251. agentex/types/messages/batch_update_params.py +16 -0
  252. agentex/types/messages/batch_update_response.py +10 -0
  253. agentex/types/shared/__init__.py +3 -0
  254. agentex/types/shared/task_message_update.py +83 -0
  255. agentex/types/span.py +36 -0
  256. agentex/types/span_create_params.py +40 -0
  257. agentex/types/span_list_params.py +12 -0
  258. agentex/types/span_list_response.py +10 -0
  259. agentex/types/span_update_params.py +37 -0
  260. agentex/types/state.py +25 -0
  261. agentex/types/state_create_params.py +16 -0
  262. agentex/types/state_list_params.py +16 -0
  263. agentex/types/state_list_response.py +10 -0
  264. agentex/types/state_update_params.py +16 -0
  265. agentex/types/task.py +23 -0
  266. agentex/types/task_delete_by_name_response.py +8 -0
  267. agentex/types/task_delete_response.py +8 -0
  268. agentex/types/task_list_response.py +10 -0
  269. agentex/types/task_message.py +33 -0
  270. agentex/types/task_message_content.py +16 -0
  271. agentex/types/task_message_content_param.py +17 -0
  272. agentex/types/task_message_delta.py +16 -0
  273. agentex/types/text_content.py +53 -0
  274. agentex/types/text_content_param.py +54 -0
  275. agentex/types/text_delta.py +14 -0
  276. agentex/types/tool_request_content.py +36 -0
  277. agentex/types/tool_request_content_param.py +37 -0
  278. agentex/types/tool_request_delta.py +18 -0
  279. agentex/types/tool_response_content.py +36 -0
  280. agentex/types/tool_response_content_param.py +36 -0
  281. agentex/types/tool_response_delta.py +18 -0
  282. agentex/types/tracker_list_params.py +16 -0
  283. agentex/types/tracker_list_response.py +10 -0
  284. agentex/types/tracker_update_params.py +19 -0
  285. agentex_sdk-0.1.0a6.dist-info/METADATA +426 -0
  286. agentex_sdk-0.1.0a6.dist-info/RECORD +289 -0
  287. agentex_sdk-0.1.0a6.dist-info/WHEEL +4 -0
  288. agentex_sdk-0.1.0a6.dist-info/entry_points.txt +2 -0
  289. 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,6 @@
1
+ from .noop_workflow import NoOpWorkflow
2
+ from .state import State
3
+ from .state_machine import StateMachine
4
+ from .state_workflow import StateWorkflow
5
+
6
+ __all__ = ["StateMachine", "StateWorkflow", "State", "NoOpWorkflow"]
@@ -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
@@ -0,0 +1,10 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+ from agentex.lib.sdk.state_machine.state_workflow import StateWorkflow
4
+
5
+
6
+ class State(BaseModel):
7
+ model_config = ConfigDict(arbitrary_types_allowed=True)
8
+
9
+ name: str
10
+ workflow: StateWorkflow