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.
- agentex/_version.py +1 -1
- agentex/lib/adk/utils/_modules/client.py +6 -5
- agentex/lib/cli/commands/agents.py +36 -2
- agentex/lib/cli/debug/__init__.py +15 -0
- agentex/lib/cli/debug/debug_config.py +116 -0
- agentex/lib/cli/debug/debug_handlers.py +174 -0
- agentex/lib/cli/handlers/agent_handlers.py +3 -2
- agentex/lib/cli/handlers/deploy_handlers.py +92 -44
- agentex/lib/cli/handlers/run_handlers.py +24 -7
- agentex/lib/cli/templates/default/Dockerfile.j2 +2 -0
- agentex/lib/cli/templates/default/pyproject.toml.j2 +0 -1
- agentex/lib/cli/templates/sync/Dockerfile.j2 +2 -0
- agentex/lib/cli/templates/sync/pyproject.toml.j2 +0 -1
- agentex/lib/cli/templates/temporal/Dockerfile.j2 +2 -0
- agentex/lib/cli/templates/temporal/project/acp.py.j2 +31 -0
- agentex/lib/cli/templates/temporal/project/run_worker.py.j2 +4 -1
- agentex/lib/cli/templates/temporal/pyproject.toml.j2 +1 -1
- agentex/lib/core/services/adk/acp/acp.py +5 -5
- agentex/lib/core/temporal/workers/worker.py +24 -0
- agentex/lib/environment_variables.py +2 -0
- agentex/lib/sdk/fastacp/base/base_acp_server.py +13 -90
- agentex/lib/utils/debug.py +63 -0
- agentex/lib/utils/registration.py +101 -0
- agentex/resources/agents.py +36 -22
- agentex/types/agent.py +7 -0
- {agentex_sdk-0.2.6.dist-info → agentex_sdk-0.2.8.dist-info}/METADATA +32 -1
- {agentex_sdk-0.2.6.dist-info → agentex_sdk-0.2.8.dist-info}/RECORD +30 -25
- {agentex_sdk-0.2.6.dist-info → agentex_sdk-0.2.8.dist-info}/WHEEL +0 -0
- {agentex_sdk-0.2.6.dist-info → agentex_sdk-0.2.8.dist-info}/entry_points.txt +0 -0
- {agentex_sdk-0.2.6.dist-info → agentex_sdk-0.2.8.dist-info}/licenses/LICENSE +0 -0
agentex/_version.py
CHANGED
@@ -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-
|
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
|
-
|
19
|
-
if
|
20
|
-
request.headers[self.header_name] =
|
21
|
-
|
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.
|
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
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
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
|
-
|
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
|
-
#
|
247
|
-
if
|
248
|
-
helm_values["env"] = convert_env_vars_dict_to_list(
|
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
|
-
|
295
|
-
|
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
|
-
|
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(
|