Skip to content

Commit 64f4214

Browse files
SWE Destroyerclaude
andcommitted
Centralize port defaults and make ports configurable
- Create src/agentex/lib/constants/ports.py as single source of truth for all default port numbers (ACP, API, Temporal, Redis, health check, debug) - Replace all hardcoded port literals across the codebase with constants - Make ports overridable via environment variables (TEMPORAL_ADDRESS, REDIS_URL, ACP_PORT, HEALTH_CHECK_PORT, AGENTEX_DEBUG_PORT) - Update Dockerfile templates to use ARG/ENV pattern for ACP_PORT so the port can be configured at Docker build time - Change health check port default from 8080 (too generic) to 9718 - Update run_handlers to read TEMPORAL_ADDRESS and REDIS_URL from env before falling back to defaults - Update cleanup_handlers to use configurable Temporal address instead of hardcoded localhost:7233 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c7162a6 commit 64f4214

28 files changed

Lines changed: 292 additions & 220 deletions

File tree

src/agentex/_client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
SyncAPIClient,
3030
AsyncAPIClient,
3131
)
32+
from .lib.constants.ports import DEFAULT_AGENTEX_API_BASE_URL
3233

3334
if TYPE_CHECKING:
3435
from .resources import spans, tasks, agents, events, states, tracker, messages, deployment_history
@@ -54,8 +55,8 @@
5455
]
5556

5657
ENVIRONMENTS: Dict[str, str] = {
57-
"production": "http://localhost:5003",
58-
"development": "http://localhost:5003",
58+
"production": DEFAULT_AGENTEX_API_BASE_URL,
59+
"development": DEFAULT_AGENTEX_API_BASE_URL,
5960
}
6061

6162

src/agentex/lib/cli/debug/debug_config.py

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
import socket
66
from enum import Enum
77

8+
from agentex.lib.constants.ports import DEFAULT_DEBUG_ACP_PORT, DEFAULT_DEBUG_WORKER_PORT
89
from agentex.lib.utils.model_utils import BaseModel
910

1011

1112
class DebugMode(str, Enum):
1213
"""Debug mode options"""
14+
1315
WORKER = "worker"
1416
ACP = "acp"
1517
BOTH = "both"
@@ -18,91 +20,73 @@ class DebugMode(str, Enum):
1820

1921
class DebugConfig(BaseModel):
2022
"""Configuration for debug mode"""
21-
23+
2224
enabled: bool = False
2325
mode: DebugMode = DebugMode.NONE
24-
port: int = 5678
26+
port: int = DEFAULT_DEBUG_WORKER_PORT
2527
wait_for_attach: bool = False
2628
auto_port: bool = True # Automatically find available port if specified port is busy
27-
29+
2830
@classmethod
2931
def create_worker_debug(
30-
cls,
31-
port: int = 5678,
32-
wait_for_attach: bool = False,
33-
auto_port: bool = True
32+
cls, port: int = DEFAULT_DEBUG_WORKER_PORT, wait_for_attach: bool = False, auto_port: bool = True
3433
) -> "DebugConfig":
3534
"""Create debug config for worker debugging"""
36-
return cls(
37-
enabled=True,
38-
mode=DebugMode.WORKER,
39-
port=port,
40-
wait_for_attach=wait_for_attach,
41-
auto_port=auto_port
42-
)
43-
35+
return cls(enabled=True, mode=DebugMode.WORKER, port=port, wait_for_attach=wait_for_attach, auto_port=auto_port)
36+
4437
@classmethod
4538
def create_acp_debug(
46-
cls,
47-
port: int = 5679,
48-
wait_for_attach: bool = False,
49-
auto_port: bool = True
39+
cls, port: int = DEFAULT_DEBUG_ACP_PORT, wait_for_attach: bool = False, auto_port: bool = True
5040
) -> "DebugConfig":
5141
"""Create debug config for ACP debugging"""
52-
return cls(
53-
enabled=True,
54-
mode=DebugMode.ACP,
55-
port=port,
56-
wait_for_attach=wait_for_attach,
57-
auto_port=auto_port
58-
)
59-
42+
return cls(enabled=True, mode=DebugMode.ACP, port=port, wait_for_attach=wait_for_attach, auto_port=auto_port)
43+
6044
@classmethod
6145
def create_both_debug(
62-
cls,
63-
worker_port: int = 5678,
64-
_acp_port: int = 5679,
46+
cls,
47+
worker_port: int = DEFAULT_DEBUG_WORKER_PORT,
48+
_acp_port: int = DEFAULT_DEBUG_ACP_PORT,
6549
wait_for_attach: bool = False,
66-
auto_port: bool = True
50+
auto_port: bool = True,
6751
) -> "DebugConfig":
6852
"""Create debug config for both worker and ACP debugging"""
6953
return cls(
7054
enabled=True,
7155
mode=DebugMode.BOTH,
7256
port=worker_port, # Primary port for worker
7357
wait_for_attach=wait_for_attach,
74-
auto_port=auto_port
58+
auto_port=auto_port,
7559
)
76-
60+
7761
def should_debug_worker(self) -> bool:
7862
"""Check if worker should be debugged"""
7963
return self.enabled and self.mode in (DebugMode.WORKER, DebugMode.BOTH)
80-
64+
8165
def should_debug_acp(self) -> bool:
8266
"""Check if ACP should be debugged"""
8367
return self.enabled and self.mode in (DebugMode.ACP, DebugMode.BOTH)
84-
68+
8569
def get_worker_port(self) -> int:
8670
"""Get port for worker debugging"""
8771
return self.port
88-
72+
8973
def get_acp_port(self) -> int:
9074
"""Get port for ACP debugging"""
9175
if self.mode == DebugMode.BOTH:
9276
return self.port + 1 # Use port + 1 for ACP when debugging both
9377
return self.port
9478

9579

96-
def find_available_port(start_port: int = 5678, max_attempts: int = 10) -> int:
80+
def find_available_port(start_port: int = DEFAULT_DEBUG_WORKER_PORT, max_attempts: int = 10) -> int:
9781
"""Find an available port starting from start_port"""
9882
for port in range(start_port, start_port + max_attempts):
9983
try:
10084
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
101-
s.bind(('localhost', port))
85+
s.bind(("localhost", port))
10286
return port
10387
except OSError:
10488
continue
105-
89+
10690
# If we can't find an available port, just return the start port
10791
# and let the debug server handle the error
10892
return start_port
@@ -112,4 +96,4 @@ def resolve_debug_port(config: DebugConfig, target_port: int) -> int:
11296
"""Resolve the actual port to use for debugging"""
11397
if config.auto_port:
11498
return find_available_port(target_port)
115-
return target_port
99+
return target_port

src/agentex/lib/cli/handlers/cleanup_handlers.py

Lines changed: 42 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from agentex import Agentex
77
from agentex.lib.utils.logging import make_logger
8+
from agentex.lib.constants.ports import DEFAULT_TEMPORAL_ADDRESS
89

910
# Import Temporal client for direct workflow termination
1011
try:
@@ -19,88 +20,84 @@
1920
def should_cleanup_on_restart() -> bool:
2021
"""
2122
Check if cleanup should be performed on restart.
22-
23+
2324
Returns True if:
2425
- ENVIRONMENT=development, OR
2526
- AUTO_CLEANUP_ON_RESTART=true
2627
"""
2728
env = os.getenv("ENVIRONMENT", "").lower()
2829
auto_cleanup = os.getenv("AUTO_CLEANUP_ON_RESTART", "true").lower()
29-
30+
3031
return env == "development" or auto_cleanup == "true"
3132

3233

33-
def cleanup_agent_workflows(
34-
agent_name: str,
35-
force: bool = False,
36-
development_only: bool = True
37-
) -> None:
34+
def cleanup_agent_workflows(agent_name: str, force: bool = False, development_only: bool = True) -> None:
3835
"""
3936
Clean up all running workflows for an agent during development.
40-
37+
4138
This cancels (graceful) all running tasks for the specified agent.
4239
When force=True, directly terminates workflows via Temporal client.
43-
40+
4441
Args:
4542
agent_name: Name of the agent to cleanup workflows for
4643
force: If True, directly terminate workflows via Temporal client
4744
development_only: Only perform cleanup in development environment
4845
"""
49-
46+
5047
# Safety check - only run in development mode by default
5148
if development_only and not force and not should_cleanup_on_restart():
5249
logger.warning("Cleanup skipped - not in development mode. Use --force to override.")
5350
return
54-
51+
5552
method = "terminate (direct)" if force else "cancel (via agent)"
5653
console.print(f"[blue]Cleaning up workflows for agent '{agent_name}' using {method}...[/blue]")
57-
54+
5855
try:
5956
client = Agentex()
60-
57+
6158
# Get all running tasks
6259
if agent_name:
6360
all_tasks = client.tasks.list(agent_name=agent_name)
6461
else:
6562
all_tasks = client.tasks.list()
66-
running_tasks = [task for task in all_tasks if hasattr(task, 'status') and task.status == "RUNNING"]
67-
63+
running_tasks = [task for task in all_tasks if hasattr(task, "status") and task.status == "RUNNING"]
64+
6865
if not running_tasks:
6966
console.print("[yellow]No running tasks found[/yellow]")
7067
return
71-
68+
7269
console.print(f"[blue]Cleaning up {len(running_tasks)} running task(s) for agent '{agent_name}'...[/blue]")
73-
70+
7471
successful_cleanups = 0
7572
total_tasks = len(running_tasks)
76-
73+
7774
for task in running_tasks:
7875
task_cleanup_success = False
79-
76+
8077
if force:
8178
# Force mode: Do both graceful RPC cancellation AND direct Temporal termination
8279
rpc_success = False
8380
temporal_success = False
84-
81+
8582
try:
8683
# First: Graceful cancellation via agent RPC (handles database/agent cleanup)
8784
cleanup_single_task(client, agent_name, task.id)
8885
logger.debug(f"Completed RPC cancellation for task {task.id}")
8986
rpc_success = True
9087
except Exception as e:
9188
logger.warning(f"RPC cancellation failed for task {task.id}: {e}")
92-
89+
9390
try:
9491
# Second: Direct Temporal termination (ensures workflow is forcefully stopped)
9592
asyncio.run(cleanup_single_task_direct(task.id))
9693
logger.debug(f"Completed Temporal termination for task {task.id}")
9794
temporal_success = True
9895
except Exception as e:
9996
logger.warning(f"Temporal termination failed for task {task.id}: {e}")
100-
97+
10198
# Count as success if either operation succeeded
10299
task_cleanup_success = rpc_success or temporal_success
103-
100+
104101
else:
105102
# Normal mode: Only graceful cancellation via agent RPC
106103
try:
@@ -109,21 +106,25 @@ def cleanup_agent_workflows(
109106
except Exception as e:
110107
logger.error(f"Failed to cleanup task {task.id}: {e}")
111108
task_cleanup_success = False
112-
109+
113110
if task_cleanup_success:
114111
successful_cleanups += 1
115112
logger.debug(f"Successfully cleaned up task {task.id}")
116113
else:
117114
logger.error(f"Failed to cleanup task {task.id}")
118115
# Don't increment successful_cleanups for actual failures
119-
116+
120117
if successful_cleanups == total_tasks:
121-
console.print(f"[green]✓ Successfully cleaned up all {successful_cleanups} task(s) for agent '{agent_name}'[/green]")
118+
console.print(
119+
f"[green]✓ Successfully cleaned up all {successful_cleanups} task(s) for agent '{agent_name}'[/green]"
120+
)
122121
elif successful_cleanups > 0:
123-
console.print(f"[yellow]⚠ Successfully cleaned up {successful_cleanups}/{total_tasks} task(s) for agent '{agent_name}'[/yellow]")
122+
console.print(
123+
f"[yellow]⚠ Successfully cleaned up {successful_cleanups}/{total_tasks} task(s) for agent '{agent_name}'[/yellow]"
124+
)
124125
else:
125126
console.print(f"[red]✗ Failed to cleanup any tasks for agent '{agent_name}'[/red]")
126-
127+
127128
except Exception as e:
128129
console.print(f"[red]Agent workflow cleanup failed: {str(e)}[/red]")
129130
logger.exception("Agent workflow cleanup failed")
@@ -133,51 +134,47 @@ def cleanup_agent_workflows(
133134
async def cleanup_single_task_direct(task_id: str) -> None:
134135
"""
135136
Directly terminate a workflow using Temporal client.
136-
137+
137138
Args:
138139
task_id: ID of the task (used as workflow_id)
139140
"""
140141
if TemporalClient is None:
141142
raise ImportError("temporalio package not available for direct workflow termination")
142-
143+
143144
try:
144-
# Connect to Temporal server (assumes default localhost:7233)
145-
client = await TemporalClient.connect("localhost:7233") # type: ignore
146-
145+
temporal_address = os.environ.get("TEMPORAL_ADDRESS", DEFAULT_TEMPORAL_ADDRESS)
146+
client = await TemporalClient.connect(temporal_address) # type: ignore
147+
147148
# Get workflow handle and terminate
148149
handle = client.get_workflow_handle(workflow_id=task_id) # type: ignore
149150
await handle.terminate() # type: ignore
150-
151+
151152
logger.debug(f"Successfully terminated workflow {task_id} via Temporal client")
152-
153+
153154
except Exception as e:
154155
# Check if the workflow was already completed - this is actually a success case
155156
if "workflow execution already completed" in str(e).lower():
156157
logger.debug(f"Workflow {task_id} was already completed - no termination needed")
157158
return # Don't raise an exception for this case
158-
159+
159160
logger.error(f"Failed to terminate workflow {task_id} via Temporal client: {e}")
160161
raise
161162

162163

163164
def cleanup_single_task(client: Agentex, agent_name: str, task_id: str) -> None:
164165
"""
165166
Clean up a single task/workflow using agent RPC cancel method.
166-
167+
167168
Args:
168-
client: Agentex client instance
169+
client: Agentex client instance
169170
agent_name: Name of the agent that owns the task
170171
task_id: ID of the task to cleanup
171172
"""
172173
try:
173174
# Use the agent RPC method to cancel the task
174-
client.agents.rpc_by_name(
175-
agent_name=agent_name,
176-
method="task/cancel",
177-
params={"task_id": task_id}
178-
)
175+
client.agents.rpc_by_name(agent_name=agent_name, method="task/cancel", params={"task_id": task_id})
179176
logger.debug(f"Successfully cancelled task {task_id} via agent '{agent_name}'")
180-
177+
181178
except Exception as e:
182179
logger.warning(f"RPC task/cancel failed for task {task_id}: {e}")
183-
raise
180+
raise

src/agentex/lib/cli/handlers/deploy_handlers.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from rich.console import Console
1212

1313
from agentex.lib.utils.logging import make_logger
14+
from agentex.lib.constants.ports import DEFAULT_ACP_PORT
1415
from agentex.lib.cli.utils.exceptions import HelmError, DeploymentError
1516
from agentex.lib.cli.utils.path_utils import PathResolutionError, calculate_docker_acp_module
1617
from agentex.lib.environment_variables import EnvVarKeys
@@ -239,12 +240,19 @@ def add_acp_command_to_helm_values(helm_values: dict[str, Any], manifest: AgentM
239240
try:
240241
docker_acp_module = calculate_docker_acp_module(manifest, manifest_path)
241242
# Create the uvicorn command with the correct module path
242-
helm_values["command"] = ["uvicorn", f"{docker_acp_module}:acp", "--host", "0.0.0.0", "--port", "8000"]
243+
helm_values["command"] = [
244+
"uvicorn",
245+
f"{docker_acp_module}:acp",
246+
"--host",
247+
"0.0.0.0",
248+
"--port",
249+
str(DEFAULT_ACP_PORT),
250+
]
243251
logger.info(f"Using dynamic ACP command: uvicorn {docker_acp_module}:acp")
244252
except (PathResolutionError, Exception) as e:
245253
# Fallback to default command structure
246254
logger.warning(f"Could not calculate dynamic ACP module ({e}), using default: project.acp")
247-
helm_values["command"] = ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]
255+
helm_values["command"] = ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", str(DEFAULT_ACP_PORT)]
248256

249257

250258
def merge_deployment_configs(

0 commit comments

Comments
 (0)