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,452 @@
1
+ import asyncio
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+
9
+ from agentex.lib.cli.handlers.cleanup_handlers import (
10
+ cleanup_agent_workflows,
11
+ should_cleanup_on_restart
12
+ )
13
+ from agentex.lib.sdk.config.agent_manifest import AgentManifest
14
+ from agentex.lib.utils.logging import make_logger
15
+
16
+ logger = make_logger(__name__)
17
+ console = Console()
18
+
19
+
20
+ class RunError(Exception):
21
+ """An error occurred during agent run"""
22
+
23
+
24
+ class ProcessManager:
25
+ """Manages multiple subprocesses with proper cleanup"""
26
+
27
+ def __init__(self):
28
+ self.processes: list[asyncio.subprocess.Process] = []
29
+ self.shutdown_event = asyncio.Event()
30
+
31
+ def add_process(self, process: asyncio.subprocess.Process):
32
+ """Add a process to be managed"""
33
+ self.processes.append(process)
34
+
35
+ async def wait_for_shutdown(self):
36
+ """Wait for shutdown signal"""
37
+ await self.shutdown_event.wait()
38
+
39
+ def shutdown(self):
40
+ """Signal shutdown and terminate all processes"""
41
+ self.shutdown_event.set()
42
+
43
+ async def cleanup_processes(self):
44
+ """Clean up all processes"""
45
+ if not self.processes:
46
+ return
47
+
48
+ console.print("\n[yellow]Shutting down processes...[/yellow]")
49
+
50
+ # Send SIGTERM to all processes
51
+ for process in self.processes:
52
+ if process.returncode is None: # Process is still running
53
+ try:
54
+ process.terminate()
55
+ except ProcessLookupError:
56
+ pass # Process already terminated
57
+
58
+ # Wait for graceful shutdown with shorter timeout
59
+ try:
60
+ await asyncio.wait_for(
61
+ asyncio.gather(*[p.wait() for p in self.processes], return_exceptions=True),
62
+ timeout=2.0, # Reduced from 5.0 seconds
63
+ )
64
+ except TimeoutError:
65
+ # Force kill if not terminated gracefully
66
+ console.print("[yellow]Force killing unresponsive processes...[/yellow]")
67
+ for process in self.processes:
68
+ if process.returncode is None:
69
+ try:
70
+ process.kill()
71
+ await asyncio.wait_for(process.wait(), timeout=1.0)
72
+ except (ProcessLookupError, TimeoutError):
73
+ pass # Process already dead or kill failed
74
+
75
+ console.print("[green]All processes stopped[/green]")
76
+
77
+
78
+ async def start_temporal_worker_with_reload(
79
+ worker_path: Path, env: dict[str, str], process_manager: ProcessManager
80
+ ) -> asyncio.Task[None]:
81
+ """Start temporal worker with auto-reload using watchfiles"""
82
+
83
+ try:
84
+ from watchfiles import awatch
85
+ except ImportError:
86
+ console.print("[yellow]watchfiles not installed, falling back to basic worker start[/yellow]")
87
+ console.print("[dim]Install with: pip install watchfiles[/dim]")
88
+ # Fallback to regular worker without reload
89
+ worker_process = await start_temporal_worker(worker_path, env)
90
+ process_manager.add_process(worker_process)
91
+ return asyncio.create_task(stream_process_output(worker_process, "WORKER"))
92
+
93
+ async def worker_runner() -> None:
94
+ current_process: asyncio.subprocess.Process | None = None
95
+ output_task: asyncio.Task[None] | None = None
96
+
97
+ console.print(f"[blue]Starting Temporal worker with auto-reload from {worker_path}...[/blue]")
98
+
99
+ async def start_worker() -> asyncio.subprocess.Process:
100
+ nonlocal current_process, output_task
101
+
102
+ # PRE-RESTART CLEANUP - NEW!
103
+ if current_process is not None:
104
+ # Extract agent name from worker path for cleanup
105
+ agent_name = worker_path.parent.parent.name
106
+
107
+ # Perform cleanup if configured
108
+ if should_cleanup_on_restart():
109
+ console.print("[yellow]Cleaning up workflows before worker restart...[/yellow]")
110
+ try:
111
+ cleanup_agent_workflows(agent_name)
112
+ except Exception as e:
113
+ logger.warning(f"Cleanup failed: {e}")
114
+ console.print(f"[yellow]⚠ Cleanup failed: {str(e)}[/yellow]")
115
+
116
+ # Clean up previous process
117
+ if current_process and current_process.returncode is None:
118
+ current_process.terminate()
119
+ try:
120
+ await asyncio.wait_for(current_process.wait(), timeout=2.0)
121
+ except asyncio.TimeoutError:
122
+ current_process.kill()
123
+ await current_process.wait()
124
+
125
+ # Cancel previous output task
126
+ if output_task:
127
+ output_task.cancel()
128
+ try:
129
+ await output_task
130
+ except asyncio.CancelledError:
131
+ pass
132
+
133
+ current_process = await start_temporal_worker(worker_path, env)
134
+ process_manager.add_process(current_process)
135
+ console.print("[green]Temporal worker started[/green]")
136
+ return current_process
137
+
138
+ try:
139
+ # Start initial worker
140
+ await start_worker()
141
+ if current_process:
142
+ output_task = asyncio.create_task(stream_process_output(current_process, "WORKER"))
143
+
144
+ # Watch for file changes
145
+ async for changes in awatch(worker_path.parent):
146
+ # Filter for Python files
147
+ py_changes = [(change, path) for change, path in changes if str(path).endswith('.py')]
148
+
149
+ if py_changes:
150
+ changed_files = [str(Path(path).relative_to(worker_path.parent)) for _, path in py_changes]
151
+ console.print(f"[yellow]File changes detected: {changed_files}[/yellow]")
152
+ console.print("[yellow]Restarting Temporal worker...[/yellow]")
153
+
154
+ # Restart worker (with cleanup handled in start_worker)
155
+ await start_worker()
156
+ if current_process:
157
+ output_task = asyncio.create_task(stream_process_output(current_process, "WORKER"))
158
+
159
+ except asyncio.CancelledError:
160
+ # Clean shutdown
161
+ if output_task:
162
+ output_task.cancel()
163
+ try:
164
+ await output_task
165
+ except asyncio.CancelledError:
166
+ pass
167
+
168
+ if current_process and current_process.returncode is None:
169
+ current_process.terminate()
170
+ try:
171
+ await asyncio.wait_for(current_process.wait(), timeout=2.0)
172
+ except asyncio.TimeoutError:
173
+ current_process.kill()
174
+ await current_process.wait()
175
+ raise
176
+
177
+ return asyncio.create_task(worker_runner())
178
+
179
+
180
+ async def start_acp_server(
181
+ acp_path: Path, port: int, env: dict[str, str]
182
+ ) -> asyncio.subprocess.Process:
183
+ """Start the ACP server process"""
184
+ # Use the actual file path instead of module path for better reload detection
185
+ cmd = [
186
+ sys.executable,
187
+ "-m",
188
+ "uvicorn",
189
+ f"{acp_path.parent.name}.acp:acp",
190
+ "--reload",
191
+ "--reload-dir",
192
+ str(acp_path.parent), # Watch the project directory specifically
193
+ "--port",
194
+ str(port),
195
+ "--host",
196
+ "0.0.0.0",
197
+ ]
198
+
199
+ console.print(f"[blue]Starting ACP server from {acp_path} on port {port}...[/blue]")
200
+ return await asyncio.create_subprocess_exec(
201
+ *cmd,
202
+ cwd=acp_path.parent.parent,
203
+ env=env,
204
+ stdout=asyncio.subprocess.PIPE,
205
+ stderr=asyncio.subprocess.STDOUT,
206
+ )
207
+
208
+
209
+ async def start_temporal_worker(
210
+ worker_path: Path, env: dict[str, str]
211
+ ) -> asyncio.subprocess.Process:
212
+ """Start the temporal worker process"""
213
+ cmd = [sys.executable, "-m", "run_worker"]
214
+
215
+ console.print(f"[blue]Starting Temporal worker from {worker_path}...[/blue]")
216
+
217
+ return await asyncio.create_subprocess_exec(
218
+ *cmd,
219
+ cwd=worker_path.parent,
220
+ env=env,
221
+ stdout=asyncio.subprocess.PIPE,
222
+ stderr=asyncio.subprocess.STDOUT,
223
+ )
224
+
225
+
226
+ async def stream_process_output(process: asyncio.subprocess.Process, prefix: str):
227
+ """Stream process output with prefix"""
228
+ try:
229
+ while True:
230
+ line = await process.stdout.readline()
231
+ if not line:
232
+ break
233
+ decoded_line = line.decode("utf-8").rstrip()
234
+ if decoded_line: # Only print non-empty lines
235
+ console.print(f"[dim]{prefix}:[/dim] {decoded_line}")
236
+ except Exception as e:
237
+ logger.debug(f"Output streaming ended for {prefix}: {e}")
238
+
239
+
240
+ async def run_agent(manifest_path: str):
241
+ """Run an agent locally from the given manifest"""
242
+
243
+ # Validate manifest exists
244
+ manifest_file = Path(manifest_path)
245
+
246
+ if not manifest_file.exists():
247
+ raise RunError(f"Manifest file not found: {manifest_path}")
248
+
249
+ # Parse manifest
250
+ try:
251
+ manifest = AgentManifest.from_yaml(file_path=manifest_path)
252
+ except Exception as e:
253
+ raise RunError(f"Failed to parse manifest: {str(e)}") from e
254
+
255
+ # Get and validate file paths
256
+ try:
257
+ file_paths = get_file_paths(manifest, manifest_path)
258
+ except Exception as e:
259
+ raise RunError(str(e)) from e
260
+
261
+ # Check if temporal agent and validate worker file
262
+ if is_temporal_agent(manifest):
263
+ if not file_paths["worker"]:
264
+ raise RunError("Temporal agent requires a worker file path to be configured")
265
+
266
+ # Create environment for subprocesses
267
+ agent_env = create_agent_environment(manifest)
268
+
269
+ # Setup process manager
270
+ process_manager = ProcessManager()
271
+
272
+ try:
273
+ console.print(
274
+ Panel.fit(
275
+ f"🚀 [bold blue]Running Agent: {manifest.agent.name}[/bold blue]",
276
+ border_style="blue",
277
+ )
278
+ )
279
+
280
+ # Start ACP server
281
+ acp_process = await start_acp_server(
282
+ file_paths["acp"], manifest.local_development.agent.port, agent_env
283
+ )
284
+ process_manager.add_process(acp_process)
285
+
286
+ # Start output streaming for ACP
287
+ acp_output_task = asyncio.create_task(stream_process_output(acp_process, "ACP"))
288
+
289
+ tasks = [acp_output_task]
290
+
291
+ # Start temporal worker if needed
292
+ if is_temporal_agent(manifest):
293
+ worker_task = await start_temporal_worker_with_reload(file_paths["worker"], agent_env, process_manager)
294
+ tasks.append(worker_task)
295
+
296
+ console.print(
297
+ f"\n[green]✓ Agent running at: http://localhost:{manifest.local_development.agent.port}[/green]"
298
+ )
299
+ console.print("[dim]Press Ctrl+C to stop[/dim]\n")
300
+
301
+ # Wait for shutdown signal or process failure
302
+ try:
303
+ await process_manager.wait_for_shutdown()
304
+ except KeyboardInterrupt:
305
+ console.print("\n[yellow]Received shutdown signal...[/yellow]")
306
+
307
+ # Cancel output streaming tasks
308
+ for task in tasks:
309
+ task.cancel()
310
+ try:
311
+ await task
312
+ except asyncio.CancelledError:
313
+ pass
314
+
315
+ except Exception as e:
316
+ logger.exception("Error running agent")
317
+ raise RunError(f"Failed to run agent: {str(e)}") from e
318
+
319
+ finally:
320
+ # Ensure cleanup happens
321
+ await process_manager.cleanup_processes()
322
+
323
+
324
+ def resolve_and_validate_path(base_path: Path, configured_path: str, file_type: str) -> Path:
325
+ """Resolve and validate a configured path"""
326
+ path_obj = Path(configured_path)
327
+
328
+ if path_obj.is_absolute():
329
+ # Absolute path - use as-is
330
+ resolved_path = path_obj
331
+ else:
332
+ # Relative path - resolve relative to manifest directory
333
+ resolved_path = (base_path / configured_path).resolve()
334
+
335
+ # Validate the file exists
336
+ if not resolved_path.exists():
337
+ raise RunError(
338
+ f"{file_type} file not found: {resolved_path}\n"
339
+ f" Configured path: {configured_path}\n"
340
+ f" Resolved from manifest: {base_path}"
341
+ )
342
+
343
+ # Validate it's actually a file
344
+ if not resolved_path.is_file():
345
+ raise RunError(f"{file_type} path is not a file: {resolved_path}")
346
+
347
+ return resolved_path
348
+
349
+
350
+ def validate_path_security(resolved_path: Path, manifest_dir: Path) -> None:
351
+ """Basic security validation for resolved paths"""
352
+ try:
353
+ # Ensure the resolved path is accessible
354
+ resolved_path.resolve()
355
+
356
+ # Optional: Add warnings for paths that go too far up
357
+ try:
358
+ # Check if path goes more than 3 levels up from manifest
359
+ relative_to_manifest = resolved_path.relative_to(manifest_dir.parent.parent.parent)
360
+ if str(relative_to_manifest).startswith(".."):
361
+ logger.warning(
362
+ f"Path goes significantly outside project structure: {resolved_path}"
363
+ )
364
+ except ValueError:
365
+ # Path is outside the tree - that's okay, just log it
366
+ logger.info(f"Using path outside manifest directory tree: {resolved_path}")
367
+
368
+ except Exception as e:
369
+ raise RunError(f"Path resolution failed: {resolved_path} - {str(e)}") from e
370
+
371
+
372
+ def get_file_paths(manifest: AgentManifest, manifest_path: str) -> dict[str, Path]:
373
+ """Get resolved file paths from manifest configuration"""
374
+ manifest_dir = Path(manifest_path).parent.resolve()
375
+
376
+ # Use configured paths or fall back to defaults for backward compatibility
377
+ if manifest.local_development and manifest.local_development.paths:
378
+ paths_config = manifest.local_development.paths
379
+
380
+ # Resolve ACP path
381
+ acp_path = resolve_and_validate_path(manifest_dir, paths_config.acp, "ACP server")
382
+ validate_path_security(acp_path, manifest_dir)
383
+
384
+ # Resolve worker path if specified
385
+ worker_path = None
386
+ if paths_config.worker:
387
+ worker_path = resolve_and_validate_path(
388
+ manifest_dir, paths_config.worker, "Temporal worker"
389
+ )
390
+ validate_path_security(worker_path, manifest_dir)
391
+ else:
392
+ # Backward compatibility: use old hardcoded structure
393
+ project_dir = manifest_dir / "project"
394
+ acp_path = project_dir / "acp.py"
395
+ worker_path = project_dir / "run_worker.py" if is_temporal_agent(manifest) else None
396
+
397
+ # Validate backward compatibility paths
398
+ if not acp_path.exists():
399
+ raise RunError(f"ACP file not found: {acp_path}")
400
+
401
+ if worker_path and not worker_path.exists():
402
+ raise RunError(f"Worker file not found: {worker_path}")
403
+
404
+ return {
405
+ "acp": acp_path,
406
+ "worker": worker_path,
407
+ "acp_dir": acp_path.parent,
408
+ "worker_dir": worker_path.parent if worker_path else None,
409
+ }
410
+
411
+
412
+ def create_agent_environment(manifest: AgentManifest) -> dict[str, str]:
413
+ """Create environment variables for agent processes without modifying os.environ"""
414
+ # Start with current environment
415
+ env = dict(os.environ)
416
+
417
+ agent_config = manifest.agent
418
+
419
+ # TODO: Combine this logic with the deploy_handlers so that we can reuse the env vars
420
+ env_vars = {
421
+ "ENVIRONMENT": "development",
422
+ "TEMPORAL_ADDRESS": "localhost:7233",
423
+ "REDIS_URL": "redis://localhost:6379",
424
+ "AGENT_NAME": manifest.agent.name,
425
+ "ACP_TYPE": manifest.agent.acp_type,
426
+ "ACP_URL": f"http://{manifest.local_development.agent.host_address}",
427
+ "ACP_PORT": str(manifest.local_development.agent.port),
428
+ }
429
+
430
+ # Add description if available
431
+ if manifest.agent.description:
432
+ env_vars["AGENT_DESCRIPTION"] = manifest.agent.description
433
+
434
+ # Add temporal-specific variables if this is a temporal agent
435
+ if manifest.agent.is_temporal_agent():
436
+ temporal_config = manifest.agent.get_temporal_workflow_config()
437
+ if temporal_config:
438
+ env_vars["WORKFLOW_NAME"] = temporal_config.name
439
+ env_vars["WORKFLOW_TASK_QUEUE"] = temporal_config.queue_name
440
+
441
+ if agent_config.env:
442
+ for key, value in agent_config.env.items():
443
+ env_vars[key] = value
444
+
445
+ env.update(env_vars)
446
+
447
+ return env
448
+
449
+
450
+ def is_temporal_agent(manifest: AgentManifest) -> bool:
451
+ """Check if this is a temporal agent"""
452
+ return manifest.agent.is_temporal_agent()