agentex-sdk 0.2.6__py3-none-any.whl → 0.2.8__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 (30) hide show
  1. agentex/_version.py +1 -1
  2. agentex/lib/adk/utils/_modules/client.py +6 -5
  3. agentex/lib/cli/commands/agents.py +36 -2
  4. agentex/lib/cli/debug/__init__.py +15 -0
  5. agentex/lib/cli/debug/debug_config.py +116 -0
  6. agentex/lib/cli/debug/debug_handlers.py +174 -0
  7. agentex/lib/cli/handlers/agent_handlers.py +3 -2
  8. agentex/lib/cli/handlers/deploy_handlers.py +92 -44
  9. agentex/lib/cli/handlers/run_handlers.py +24 -7
  10. agentex/lib/cli/templates/default/Dockerfile.j2 +2 -0
  11. agentex/lib/cli/templates/default/pyproject.toml.j2 +0 -1
  12. agentex/lib/cli/templates/sync/Dockerfile.j2 +2 -0
  13. agentex/lib/cli/templates/sync/pyproject.toml.j2 +0 -1
  14. agentex/lib/cli/templates/temporal/Dockerfile.j2 +2 -0
  15. agentex/lib/cli/templates/temporal/project/acp.py.j2 +31 -0
  16. agentex/lib/cli/templates/temporal/project/run_worker.py.j2 +4 -1
  17. agentex/lib/cli/templates/temporal/pyproject.toml.j2 +1 -1
  18. agentex/lib/core/services/adk/acp/acp.py +5 -5
  19. agentex/lib/core/temporal/workers/worker.py +24 -0
  20. agentex/lib/environment_variables.py +2 -0
  21. agentex/lib/sdk/fastacp/base/base_acp_server.py +13 -90
  22. agentex/lib/utils/debug.py +63 -0
  23. agentex/lib/utils/registration.py +101 -0
  24. agentex/resources/agents.py +36 -22
  25. agentex/types/agent.py +7 -0
  26. {agentex_sdk-0.2.6.dist-info → agentex_sdk-0.2.8.dist-info}/METADATA +32 -1
  27. {agentex_sdk-0.2.6.dist-info → agentex_sdk-0.2.8.dist-info}/RECORD +30 -25
  28. {agentex_sdk-0.2.6.dist-info → agentex_sdk-0.2.8.dist-info}/WHEEL +0 -0
  29. {agentex_sdk-0.2.6.dist-info → agentex_sdk-0.2.8.dist-info}/entry_points.txt +0 -0
  30. {agentex_sdk-0.2.6.dist-info → agentex_sdk-0.2.8.dist-info}/licenses/LICENSE +0 -0
agentex/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "agentex"
4
- __version__ = "0.2.6" # x-release-please-version
4
+ __version__ = "0.2.8" # x-release-please-version
@@ -8,17 +8,18 @@ logger = make_logger(__name__)
8
8
 
9
9
 
10
10
  class EnvAuth(httpx.Auth):
11
- def __init__(self, header_name="x-agent-identity"):
11
+ def __init__(self, header_name="x-agent-api-key"):
12
12
  self.header_name = header_name
13
13
 
14
14
  def auth_flow(self, request):
15
15
  # This gets called for every request
16
16
  env_vars = EnvironmentVariables.refresh()
17
17
  if env_vars:
18
- agent_id = env_vars.AGENT_ID
19
- if agent_id:
20
- request.headers[self.header_name] = agent_id
21
- logger.info(f"Adding header {self.header_name}:{agent_id}")
18
+ agent_api_key = env_vars.AGENT_API_KEY
19
+ if agent_api_key:
20
+ request.headers[self.header_name] = agent_api_key
21
+ masked_key = agent_api_key[-4:] if agent_api_key and len(agent_api_key) > 4 else "****"
22
+ logger.info(f"Adding header {self.header_name}:{masked_key}")
22
23
  yield request
23
24
 
24
25
 
@@ -11,6 +11,7 @@ from agentex.lib.cli.handlers.agent_handlers import (
11
11
  build_agent,
12
12
  run_agent,
13
13
  )
14
+ from agentex.lib.cli.debug import DebugConfig, DebugMode
14
15
  from agentex.lib.cli.handlers.cleanup_handlers import cleanup_agent_workflows
15
16
  from agentex.lib.cli.handlers.deploy_handlers import (
16
17
  DeploymentError,
@@ -171,7 +172,13 @@ def run(
171
172
  False,
172
173
  help="Clean up existing workflows for this agent before starting"
173
174
  ),
174
- ):
175
+ # Debug options
176
+ debug: bool = typer.Option(False, help="Enable debug mode for both worker and ACP (disables auto-reload)"),
177
+ debug_worker: bool = typer.Option(False, help="Enable debug mode for temporal worker only"),
178
+ debug_acp: bool = typer.Option(False, help="Enable debug mode for ACP server only"),
179
+ debug_port: int = typer.Option(5678, help="Port for remote debugging (worker uses this, ACP uses port+1)"),
180
+ wait_for_debugger: bool = typer.Option(False, help="Wait for debugger to attach before starting"),
181
+ ) -> None:
175
182
  """
176
183
  Run an agent locally from the given manifest.
177
184
  """
@@ -196,8 +203,35 @@ def run(
196
203
  console.print(f"[yellow]⚠ Pre-run cleanup failed: {str(e)}[/yellow]")
197
204
  logger.warning(f"Pre-run cleanup failed: {e}")
198
205
 
206
+ # Create debug configuration based on CLI flags
207
+ debug_config = None
208
+ if debug or debug_worker or debug_acp:
209
+ # Determine debug mode
210
+ if debug:
211
+ mode = DebugMode.BOTH
212
+ elif debug_worker and debug_acp:
213
+ mode = DebugMode.BOTH
214
+ elif debug_worker:
215
+ mode = DebugMode.WORKER
216
+ elif debug_acp:
217
+ mode = DebugMode.ACP
218
+ else:
219
+ mode = DebugMode.NONE
220
+
221
+ debug_config = DebugConfig(
222
+ enabled=True,
223
+ mode=mode,
224
+ port=debug_port,
225
+ wait_for_attach=wait_for_debugger,
226
+ auto_port=False # Use fixed port to match VS Code launch.json
227
+ )
228
+
229
+ console.print(f"[blue]🐛 Debug mode enabled: {mode.value}[/blue]")
230
+ if wait_for_debugger:
231
+ console.print("[yellow]⏳ Processes will wait for debugger attachment[/yellow]")
232
+
199
233
  try:
200
- run_agent(manifest_path=manifest)
234
+ run_agent(manifest_path=manifest, debug_config=debug_config)
201
235
  except Exception as e:
202
236
  typer.echo(f"Error running agent: {str(e)}", err=True)
203
237
  logger.exception("Error running agent")
@@ -0,0 +1,15 @@
1
+ """
2
+ Debug functionality for AgentEx CLI
3
+
4
+ Provides debug support for temporal workers and ACP servers during local development.
5
+ """
6
+
7
+ from .debug_config import DebugConfig, DebugMode
8
+ from .debug_handlers import start_acp_server_debug, start_temporal_worker_debug
9
+
10
+ __all__ = [
11
+ "DebugConfig",
12
+ "DebugMode",
13
+ "start_acp_server_debug",
14
+ "start_temporal_worker_debug",
15
+ ]
@@ -0,0 +1,116 @@
1
+ """
2
+ Debug configuration models for AgentEx CLI debugging.
3
+ """
4
+
5
+ import socket
6
+ from enum import Enum
7
+ from typing import Optional
8
+
9
+ from agentex.lib.utils.model_utils import BaseModel
10
+
11
+
12
+ class DebugMode(str, Enum):
13
+ """Debug mode options"""
14
+ WORKER = "worker"
15
+ ACP = "acp"
16
+ BOTH = "both"
17
+ NONE = "none"
18
+
19
+
20
+ class DebugConfig(BaseModel):
21
+ """Configuration for debug mode"""
22
+
23
+ enabled: bool = False
24
+ mode: DebugMode = DebugMode.NONE
25
+ port: int = 5678
26
+ wait_for_attach: bool = False
27
+ auto_port: bool = True # Automatically find available port if specified port is busy
28
+
29
+ @classmethod
30
+ def create_worker_debug(
31
+ cls,
32
+ port: int = 5678,
33
+ wait_for_attach: bool = False,
34
+ auto_port: bool = True
35
+ ) -> "DebugConfig":
36
+ """Create debug config for worker debugging"""
37
+ return cls(
38
+ enabled=True,
39
+ mode=DebugMode.WORKER,
40
+ port=port,
41
+ wait_for_attach=wait_for_attach,
42
+ auto_port=auto_port
43
+ )
44
+
45
+ @classmethod
46
+ def create_acp_debug(
47
+ cls,
48
+ port: int = 5679,
49
+ wait_for_attach: bool = False,
50
+ auto_port: bool = True
51
+ ) -> "DebugConfig":
52
+ """Create debug config for ACP debugging"""
53
+ return cls(
54
+ enabled=True,
55
+ mode=DebugMode.ACP,
56
+ port=port,
57
+ wait_for_attach=wait_for_attach,
58
+ auto_port=auto_port
59
+ )
60
+
61
+ @classmethod
62
+ def create_both_debug(
63
+ cls,
64
+ worker_port: int = 5678,
65
+ acp_port: int = 5679,
66
+ wait_for_attach: bool = False,
67
+ auto_port: bool = True
68
+ ) -> "DebugConfig":
69
+ """Create debug config for both worker and ACP debugging"""
70
+ return cls(
71
+ enabled=True,
72
+ mode=DebugMode.BOTH,
73
+ port=worker_port, # Primary port for worker
74
+ wait_for_attach=wait_for_attach,
75
+ auto_port=auto_port
76
+ )
77
+
78
+ def should_debug_worker(self) -> bool:
79
+ """Check if worker should be debugged"""
80
+ return self.enabled and self.mode in (DebugMode.WORKER, DebugMode.BOTH)
81
+
82
+ def should_debug_acp(self) -> bool:
83
+ """Check if ACP should be debugged"""
84
+ return self.enabled and self.mode in (DebugMode.ACP, DebugMode.BOTH)
85
+
86
+ def get_worker_port(self) -> int:
87
+ """Get port for worker debugging"""
88
+ return self.port
89
+
90
+ def get_acp_port(self) -> int:
91
+ """Get port for ACP debugging"""
92
+ if self.mode == DebugMode.BOTH:
93
+ return self.port + 1 # Use port + 1 for ACP when debugging both
94
+ return self.port
95
+
96
+
97
+ def find_available_port(start_port: int = 5678, max_attempts: int = 10) -> int:
98
+ """Find an available port starting from start_port"""
99
+ for port in range(start_port, start_port + max_attempts):
100
+ try:
101
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
102
+ s.bind(('localhost', port))
103
+ return port
104
+ except OSError:
105
+ continue
106
+
107
+ # If we can't find an available port, just return the start port
108
+ # and let the debug server handle the error
109
+ return start_port
110
+
111
+
112
+ def resolve_debug_port(config: DebugConfig, target_port: int) -> int:
113
+ """Resolve the actual port to use for debugging"""
114
+ if config.auto_port:
115
+ return find_available_port(target_port)
116
+ return target_port
@@ -0,0 +1,174 @@
1
+ """
2
+ Debug process handlers for AgentEx CLI.
3
+
4
+ Provides debug-enabled versions of ACP server and temporal worker startup.
5
+ """
6
+
7
+ import asyncio
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Dict, TYPE_CHECKING
11
+
12
+ from rich.console import Console
13
+
14
+ if TYPE_CHECKING:
15
+ import asyncio.subprocess
16
+
17
+ from .debug_config import DebugConfig, resolve_debug_port
18
+ from agentex.lib.utils.logging import make_logger
19
+
20
+ logger = make_logger(__name__)
21
+ console = Console()
22
+
23
+
24
+ async def start_temporal_worker_debug(
25
+ worker_path: Path,
26
+ env: Dict[str, str],
27
+ debug_config: DebugConfig
28
+ ):
29
+ """Start temporal worker with debug support"""
30
+
31
+ if not debug_config.should_debug_worker():
32
+ raise ValueError("Debug config is not configured for worker debugging")
33
+
34
+ # Resolve the actual debug port
35
+ debug_port = resolve_debug_port(debug_config, debug_config.get_worker_port())
36
+
37
+ # Add debug environment variables
38
+ debug_env = env.copy()
39
+ debug_env.update({
40
+ "AGENTEX_DEBUG_ENABLED": "true",
41
+ "AGENTEX_DEBUG_PORT": str(debug_port),
42
+ "AGENTEX_DEBUG_WAIT_FOR_ATTACH": str(debug_config.wait_for_attach).lower(),
43
+ "AGENTEX_DEBUG_TYPE": "worker"
44
+ })
45
+
46
+ # Start the worker process
47
+ # For debugging, use absolute path to run_worker.py to run from workspace root
48
+ worker_script = worker_path.parent / "run_worker.py"
49
+ cmd = [sys.executable, str(worker_script)]
50
+
51
+ console.print(f"[blue]🐛 Starting Temporal worker in debug mode[/blue]")
52
+ console.print(f"[yellow]📡 Debug server will listen on port {debug_port}[/yellow]")
53
+ console.print(f"[green]✓ VS Code should connect to: localhost:{debug_port}[/green]")
54
+
55
+ if debug_config.wait_for_attach:
56
+ console.print(f"[yellow]⏳ Worker will wait for debugger to attach[/yellow]")
57
+
58
+ console.print(f"[dim]💡 In your IDE: Attach to localhost:{debug_port}[/dim]")
59
+ console.print(f"[dim]🔧 If connection fails, check that VS Code launch.json uses port {debug_port}[/dim]")
60
+
61
+ return await asyncio.create_subprocess_exec(
62
+ *cmd,
63
+ cwd=Path.cwd(), # Run from current working directory (workspace root)
64
+ env=debug_env,
65
+ stdout=asyncio.subprocess.PIPE,
66
+ stderr=asyncio.subprocess.STDOUT,
67
+ )
68
+
69
+
70
+ async def start_acp_server_debug(
71
+ acp_path: Path,
72
+ port: int,
73
+ env: Dict[str, str],
74
+ debug_config: DebugConfig
75
+ ):
76
+ """Start ACP server with debug support"""
77
+
78
+ if not debug_config.should_debug_acp():
79
+ raise ValueError("Debug config is not configured for ACP debugging")
80
+
81
+ # Resolve the actual debug port
82
+ debug_port = resolve_debug_port(debug_config, debug_config.get_acp_port())
83
+
84
+ # Add debug environment variables
85
+ debug_env = env.copy()
86
+ debug_env.update({
87
+ "AGENTEX_DEBUG_ENABLED": "true",
88
+ "AGENTEX_DEBUG_PORT": str(debug_port),
89
+ "AGENTEX_DEBUG_WAIT_FOR_ATTACH": str(debug_config.wait_for_attach).lower(),
90
+ "AGENTEX_DEBUG_TYPE": "acp"
91
+ })
92
+
93
+ # Disable uvicorn auto-reload in debug mode to prevent conflicts
94
+ cmd = [
95
+ sys.executable,
96
+ "-m",
97
+ "uvicorn",
98
+ f"{acp_path.parent.name}.acp:acp",
99
+ "--port",
100
+ str(port),
101
+ "--host",
102
+ "0.0.0.0",
103
+ # Note: No --reload flag when debugging
104
+ ]
105
+
106
+ console.print(f"[blue]🐛 Starting ACP server in debug mode[/blue]")
107
+ console.print(f"[yellow]📡 Debug server will listen on port {debug_port}[/yellow]")
108
+
109
+ if debug_config.wait_for_attach:
110
+ console.print(f"[yellow]⏳ ACP server will wait for debugger to attach[/yellow]")
111
+
112
+ console.print(f"[dim]💡 In your IDE: Attach to localhost:{debug_port}[/dim]")
113
+
114
+ return await asyncio.create_subprocess_exec(
115
+ *cmd,
116
+ cwd=acp_path.parent.parent,
117
+ env=debug_env,
118
+ stdout=asyncio.subprocess.PIPE,
119
+ stderr=asyncio.subprocess.STDOUT,
120
+ )
121
+
122
+
123
+ def create_debug_startup_script() -> str:
124
+ """Create a Python script snippet for debug initialization"""
125
+ return '''
126
+ import os
127
+ import sys
128
+
129
+ # Debug initialization for AgentEx
130
+ if os.getenv("AGENTEX_DEBUG_ENABLED") == "true":
131
+ try:
132
+ import debugpy
133
+ debug_port = int(os.getenv("AGENTEX_DEBUG_PORT", "5678"))
134
+ debug_type = os.getenv("AGENTEX_DEBUG_TYPE", "unknown")
135
+ wait_for_attach = os.getenv("AGENTEX_DEBUG_WAIT_FOR_ATTACH", "false").lower() == "true"
136
+
137
+ # Configure debugpy
138
+ debugpy.configure(subProcess=False)
139
+ debugpy.listen(debug_port)
140
+
141
+ print(f"🐛 [{debug_type.upper()}] Debug server listening on port {debug_port}")
142
+
143
+ if wait_for_attach:
144
+ print(f"⏳ [{debug_type.upper()}] Waiting for debugger to attach...")
145
+ debugpy.wait_for_client()
146
+ print(f"✅ [{debug_type.upper()}] Debugger attached!")
147
+ else:
148
+ print(f"📡 [{debug_type.upper()}] Ready for debugger attachment")
149
+
150
+ except ImportError:
151
+ print("❌ debugpy not available. Install with: pip install debugpy")
152
+ sys.exit(1)
153
+ except Exception as e:
154
+ print(f"❌ Debug setup failed: {e}")
155
+ sys.exit(1)
156
+ '''
157
+
158
+
159
+ def inject_debug_code_to_worker_template() -> str:
160
+ """Generate debug code to inject into worker template"""
161
+ return """
162
+ # === DEBUG SETUP (Auto-generated by AgentEx CLI) ===
163
+ """ + create_debug_startup_script() + """
164
+ # === END DEBUG SETUP ===
165
+ """
166
+
167
+
168
+ def inject_debug_code_to_acp_template() -> str:
169
+ """Generate debug code to inject into ACP template"""
170
+ return """
171
+ # === DEBUG SETUP (Auto-generated by AgentEx CLI) ===
172
+ """ + create_debug_startup_script() + """
173
+ # === END DEBUG SETUP ===
174
+ """
@@ -7,6 +7,7 @@ from rich.console import Console
7
7
 
8
8
  from agentex.lib.cli.handlers.run_handlers import RunError
9
9
  from agentex.lib.cli.handlers.run_handlers import run_agent as _run_agent
10
+ from agentex.lib.cli.debug import DebugConfig
10
11
  from agentex.lib.sdk.config.agent_manifest import AgentManifest
11
12
  from agentex.lib.utils.logging import make_logger
12
13
 
@@ -126,7 +127,7 @@ def build_agent(
126
127
  return image_name
127
128
 
128
129
 
129
- def run_agent(manifest_path: str):
130
+ def run_agent(manifest_path: str, debug_config: "DebugConfig | None" = None):
130
131
  """Run an agent locally from the given manifest"""
131
132
  import asyncio
132
133
  import signal
@@ -152,7 +153,7 @@ def run_agent(manifest_path: str):
152
153
  signal.signal(signal.SIGTERM, signal_handler)
153
154
 
154
155
  try:
155
- asyncio.run(_run_agent(manifest_path))
156
+ asyncio.run(_run_agent(manifest_path, debug_config))
156
157
  except KeyboardInterrupt:
157
158
  print("Shutdown completed.")
158
159
  sys.exit(0)
@@ -22,7 +22,7 @@ logger = make_logger(__name__)
22
22
  console = Console()
23
23
 
24
24
  TEMPORAL_WORKER_KEY = "temporal-worker"
25
- AGENTEX_AGENTS_HELM_CHART_VERSION = "0.1.4-v1-beta"
25
+ AGENTEX_AGENTS_HELM_CHART_VERSION = "0.1.6-v1-beta"
26
26
 
27
27
 
28
28
  class InputDeployOverrides(BaseModel):
@@ -133,7 +133,7 @@ def merge_deployment_configs(
133
133
  raise DeploymentError("Repository and image tag are required")
134
134
 
135
135
  # Start with global configuration
136
- helm_values = {
136
+ helm_values: dict[str, Any] = {
137
137
  "global": {
138
138
  "image": {
139
139
  "repository": repository,
@@ -157,55 +157,76 @@ def merge_deployment_configs(
157
157
  "memory": manifest.deployment.global_config.resources.limits.memory,
158
158
  },
159
159
  },
160
+ # Enable autoscaling by default for production deployments
161
+ "autoscaling": {
162
+ "enabled": True,
163
+ "minReplicas": 1,
164
+ "maxReplicas": 10,
165
+ "targetCPUUtilizationPercentage": 50,
166
+ },
160
167
  }
161
168
 
162
169
  # Handle temporal configuration using new helper methods
163
170
  if agent_config.is_temporal_agent():
164
171
  temporal_config = agent_config.get_temporal_workflow_config()
165
172
  if temporal_config:
166
- helm_values[TEMPORAL_WORKER_KEY] = {}
173
+ helm_values[TEMPORAL_WORKER_KEY] = {
174
+ "enabled": True,
175
+ # Enable autoscaling for temporal workers as well
176
+ "autoscaling": {
177
+ "enabled": True,
178
+ "minReplicas": 1,
179
+ "maxReplicas": 10,
180
+ "targetCPUUtilizationPercentage": 50,
181
+ },
182
+ }
167
183
  helm_values["global"]["workflow"] = {
168
184
  "name": temporal_config.name,
169
185
  "taskQueue": temporal_config.queue_name,
170
186
  }
171
- helm_values[TEMPORAL_WORKER_KEY]["enabled"] = True
172
187
 
173
- secret_env_vars = []
174
- if agent_config.credentials:
175
- for credential in agent_config.credentials:
176
- secret_env_vars.append(
177
- {
178
- "name": credential.env_var_name,
179
- "secretName": credential.secret_name,
180
- "secretKey": credential.secret_key,
181
- }
182
- )
183
-
184
- helm_values["secretEnvVars"] = secret_env_vars
185
- if TEMPORAL_WORKER_KEY in helm_values:
186
- helm_values[TEMPORAL_WORKER_KEY]["secretEnvVars"] = secret_env_vars
187
-
188
- # Set the agent_config env vars first to the helm values and so then it can be overriden by the cluster config
188
+ # Collect all environment variables with conflict detection
189
+ all_env_vars: dict[str, str] = {}
190
+ secret_env_vars: list[dict[str, str]] = []
191
+
192
+ # Start with agent_config env vars
189
193
  if agent_config.env:
190
- helm_values["env"] = agent_config.env
191
- if TEMPORAL_WORKER_KEY in helm_values:
192
- helm_values[TEMPORAL_WORKER_KEY]["env"] = agent_config.env
194
+ all_env_vars.update(agent_config.env)
193
195
 
194
196
  # Add auth principal env var if manifest principal is set
195
197
  encoded_principal = _encode_principal_context(manifest)
196
198
  if encoded_principal:
197
- if "env" not in helm_values:
198
- helm_values["env"] = {}
199
- helm_values["env"][EnvVarKeys.AUTH_PRINCIPAL_B64.value] = encoded_principal
199
+ all_env_vars[EnvVarKeys.AUTH_PRINCIPAL_B64.value] = encoded_principal
200
200
 
201
- if manifest.deployment and manifest.deployment.imagePullSecrets:
202
- pull_secrets = [
203
- pull_secret.to_dict()
204
- for pull_secret in manifest.deployment.imagePullSecrets
205
- ]
206
- helm_values["global"]["imagePullSecrets"] = pull_secrets
207
- # TODO: Remove this once i bump the chart version again
208
- helm_values["imagePullSecrets"] = pull_secrets
201
+ # Handle credentials and check for conflicts
202
+ if agent_config.credentials:
203
+ for credential in agent_config.credentials:
204
+ # Handle both CredentialMapping objects and legacy dict format
205
+ if isinstance(credential, dict):
206
+ env_var_name = credential["env_var_name"]
207
+ secret_name = credential["secret_name"]
208
+ secret_key = credential["secret_key"]
209
+ else:
210
+ env_var_name = credential.env_var_name
211
+ secret_name = credential.secret_name
212
+ secret_key = credential.secret_key
213
+
214
+ # Check if the environment variable name conflicts with existing env vars
215
+ if env_var_name in all_env_vars:
216
+ logger.warning(
217
+ f"Environment variable '{env_var_name}' is defined in both "
218
+ f"env and secretEnvVars. The secret value will take precedence."
219
+ )
220
+ # Remove from regular env vars since secret takes precedence
221
+ del all_env_vars[env_var_name]
222
+
223
+ secret_env_vars.append(
224
+ {
225
+ "name": env_var_name,
226
+ "secretName": secret_name,
227
+ "secretKey": secret_key,
228
+ }
229
+ )
209
230
 
210
231
  # Apply cluster-specific overrides
211
232
  if cluster_config:
@@ -236,23 +257,50 @@ def merge_deployment_configs(
236
257
  }
237
258
  )
238
259
 
260
+ # Handle cluster env vars with conflict detection
239
261
  if cluster_config.env:
240
- helm_values["env"] = cluster_config.env
262
+ # Convert cluster env list to dict for easier conflict detection
263
+ cluster_env_dict = {env_var["name"]: env_var["value"] for env_var in cluster_config.env}
264
+
265
+ # Check for conflicts with secret env vars
266
+ for secret_env_var in secret_env_vars:
267
+ if secret_env_var["name"] in cluster_env_dict:
268
+ logger.warning(
269
+ f"Environment variable '{secret_env_var['name']}' is defined in both "
270
+ f"cluster config env and secretEnvVars. The secret value will take precedence."
271
+ )
272
+ del cluster_env_dict[secret_env_var["name"]]
273
+
274
+ # Update all_env_vars with cluster overrides
275
+ all_env_vars.update(cluster_env_dict)
241
276
 
242
277
  # Apply additional arbitrary overrides
243
278
  if cluster_config.additional_overrides:
244
279
  _deep_merge(helm_values, cluster_config.additional_overrides)
245
280
 
246
- # Convert the env vars to a list of dictionaries
247
- if "env" in helm_values:
248
- helm_values["env"] = convert_env_vars_dict_to_list(helm_values["env"])
249
-
250
- # Convert the temporal worker env vars to a list of dictionaries
251
- if TEMPORAL_WORKER_KEY in helm_values and "env" in helm_values[TEMPORAL_WORKER_KEY]:
252
- helm_values[TEMPORAL_WORKER_KEY]["env"] = convert_env_vars_dict_to_list(
253
- helm_values[TEMPORAL_WORKER_KEY]["env"]
254
- )
281
+ # Set final environment variables
282
+ if all_env_vars:
283
+ helm_values["env"] = convert_env_vars_dict_to_list(all_env_vars)
255
284
 
285
+ if secret_env_vars:
286
+ helm_values["secretEnvVars"] = secret_env_vars
287
+
288
+ # Set environment variables for temporal worker if enabled
289
+ if TEMPORAL_WORKER_KEY in helm_values:
290
+ if all_env_vars:
291
+ helm_values[TEMPORAL_WORKER_KEY]["env"] = convert_env_vars_dict_to_list(all_env_vars)
292
+ if secret_env_vars:
293
+ helm_values[TEMPORAL_WORKER_KEY]["secretEnvVars"] = secret_env_vars
294
+
295
+ # Handle image pull secrets
296
+ if manifest.deployment and manifest.deployment.imagePullSecrets:
297
+ pull_secrets = [
298
+ pull_secret.to_dict()
299
+ for pull_secret in manifest.deployment.imagePullSecrets
300
+ ]
301
+ helm_values["global"]["imagePullSecrets"] = pull_secrets
302
+ helm_values["imagePullSecrets"] = pull_secrets
303
+
256
304
  # Add dynamic ACP command based on manifest configuration
257
305
  add_acp_command_to_helm_values(helm_values, manifest, manifest_path)
258
306
 
@@ -18,6 +18,9 @@ from agentex.lib.cli.utils.path_utils import (
18
18
 
19
19
  from agentex.lib.environment_variables import EnvVarKeys
20
20
  from agentex.lib.sdk.config.agent_manifest import AgentManifest
21
+
22
+ # Import debug functionality
23
+ from agentex.lib.cli.debug import DebugConfig, start_acp_server_debug, start_temporal_worker_debug
21
24
  from agentex.lib.utils.logging import make_logger
22
25
 
23
26
  logger = make_logger(__name__)
@@ -249,7 +252,7 @@ async def stream_process_output(process: asyncio.subprocess.Process, prefix: str
249
252
  logger.debug(f"Output streaming ended for {prefix}: {e}")
250
253
 
251
254
 
252
- async def run_agent(manifest_path: str):
255
+ async def run_agent(manifest_path: str, debug_config: "DebugConfig | None" = None):
253
256
  """Run an agent locally from the given manifest"""
254
257
 
255
258
  # Validate manifest exists
@@ -289,11 +292,16 @@ async def run_agent(manifest_path: str):
289
292
  )
290
293
  )
291
294
 
292
- # Start ACP server
295
+ # Start ACP server (with debug support if enabled)
293
296
  manifest_dir = Path(manifest_path).parent
294
- acp_process = await start_acp_server(
295
- file_paths["acp"], manifest.local_development.agent.port, agent_env, manifest_dir
296
- )
297
+ if debug_config and debug_config.should_debug_acp():
298
+ acp_process = await start_acp_server_debug(
299
+ file_paths["acp"], manifest.local_development.agent.port, agent_env, debug_config
300
+ )
301
+ else:
302
+ acp_process = await start_acp_server(
303
+ file_paths["acp"], manifest.local_development.agent.port, agent_env, manifest_dir
304
+ )
297
305
  process_manager.add_process(acp_process)
298
306
 
299
307
  # Start output streaming for ACP
@@ -301,9 +309,18 @@ async def run_agent(manifest_path: str):
301
309
 
302
310
  tasks = [acp_output_task]
303
311
 
304
- # Start temporal worker if needed
312
+ # Start temporal worker if needed (with debug support if enabled)
305
313
  if is_temporal_agent(manifest) and file_paths["worker"]:
306
- worker_task = await start_temporal_worker_with_reload(file_paths["worker"], agent_env, process_manager)
314
+ if debug_config and debug_config.should_debug_worker():
315
+ # In debug mode, start worker without auto-reload to prevent conflicts
316
+ worker_process = await start_temporal_worker_debug(
317
+ file_paths["worker"], agent_env, debug_config
318
+ )
319
+ process_manager.add_process(worker_process)
320
+ worker_task = asyncio.create_task(stream_process_output(worker_process, "WORKER"))
321
+ else:
322
+ # Normal mode with auto-reload
323
+ worker_task = await start_temporal_worker_with_reload(file_paths["worker"], agent_env, process_manager)
307
324
  tasks.append(worker_task)
308
325
 
309
326
  console.print(
@@ -15,6 +15,8 @@ RUN apt-get update && apt-get install -y \
15
15
  gcc \
16
16
  cmake \
17
17
  netcat-openbsd \
18
+ node \
19
+ npm \
18
20
  && apt-get clean \
19
21
  && rm -rf /var/lib/apt/lists/*
20
22