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
@@ -1,4 +1,35 @@
|
|
1
1
|
import os
|
2
|
+
import sys
|
3
|
+
|
4
|
+
# === DEBUG SETUP (AgentEx CLI Debug Support) ===
|
5
|
+
if os.getenv("AGENTEX_DEBUG_ENABLED") == "true":
|
6
|
+
try:
|
7
|
+
import debugpy
|
8
|
+
debug_port = int(os.getenv("AGENTEX_DEBUG_PORT", "5679"))
|
9
|
+
debug_type = os.getenv("AGENTEX_DEBUG_TYPE", "acp")
|
10
|
+
wait_for_attach = os.getenv("AGENTEX_DEBUG_WAIT_FOR_ATTACH", "false").lower() == "true"
|
11
|
+
|
12
|
+
# Configure debugpy
|
13
|
+
debugpy.configure(subProcess=False)
|
14
|
+
debugpy.listen(debug_port)
|
15
|
+
|
16
|
+
print(f"🐛 [{debug_type.upper()}] Debug server listening on port {debug_port}")
|
17
|
+
|
18
|
+
if wait_for_attach:
|
19
|
+
print(f"⏳ [{debug_type.upper()}] Waiting for debugger to attach...")
|
20
|
+
debugpy.wait_for_client()
|
21
|
+
print(f"✅ [{debug_type.upper()}] Debugger attached!")
|
22
|
+
else:
|
23
|
+
print(f"📡 [{debug_type.upper()}] Ready for debugger attachment")
|
24
|
+
|
25
|
+
except ImportError:
|
26
|
+
print("❌ debugpy not available. Install with: pip install debugpy")
|
27
|
+
sys.exit(1)
|
28
|
+
except Exception as e:
|
29
|
+
print(f"❌ Debug setup failed: {e}")
|
30
|
+
sys.exit(1)
|
31
|
+
# === END DEBUG SETUP ===
|
32
|
+
|
2
33
|
from agentex.lib.sdk.fastacp.fastacp import FastACP
|
3
34
|
from agentex.lib.types.fastacp import TemporalACPConfig
|
4
35
|
|
@@ -1,9 +1,9 @@
|
|
1
1
|
import asyncio
|
2
|
-
import os
|
3
2
|
|
4
3
|
from agentex.lib.core.temporal.activities import get_all_activities
|
5
4
|
from agentex.lib.core.temporal.workers.worker import AgentexWorker
|
6
5
|
from agentex.lib.utils.logging import make_logger
|
6
|
+
from agentex.lib.utils.debug import setup_debug_if_enabled
|
7
7
|
from agentex.lib.environment_variables import EnvironmentVariables
|
8
8
|
|
9
9
|
from workflow import {{ workflow_class }}
|
@@ -15,6 +15,9 @@ logger = make_logger(__name__)
|
|
15
15
|
|
16
16
|
|
17
17
|
async def main():
|
18
|
+
# Setup debug mode if enabled
|
19
|
+
setup_debug_if_enabled()
|
20
|
+
|
18
21
|
task_queue_name = environment_variables.WORKFLOW_TASK_QUEUE
|
19
22
|
if task_queue_name is None:
|
20
23
|
raise ValueError("WORKFLOW_TASK_QUEUE is not set")
|
@@ -6,7 +6,6 @@ build-backend = "hatchling.build"
|
|
6
6
|
name = "{{ project_name }}"
|
7
7
|
version = "0.1.0"
|
8
8
|
description = "{{ description }}"
|
9
|
-
readme = "README.md"
|
10
9
|
requires-python = ">=3.12"
|
11
10
|
dependencies = [
|
12
11
|
"agentex-sdk",
|
@@ -20,6 +19,7 @@ dev = [
|
|
20
19
|
"black",
|
21
20
|
"isort",
|
22
21
|
"flake8",
|
22
|
+
"debugpy>=1.8.15",
|
23
23
|
]
|
24
24
|
|
25
25
|
[tool.hatch.build.targets.wheel]
|
@@ -64,7 +64,7 @@ class ACPService:
|
|
64
64
|
else:
|
65
65
|
raise ValueError("Either agent_name or agent_id must be provided")
|
66
66
|
|
67
|
-
task_entry = Task.model_validate(json_rpc_response
|
67
|
+
task_entry = Task.model_validate(json_rpc_response.result)
|
68
68
|
if span:
|
69
69
|
span.output = task_entry.model_dump()
|
70
70
|
return task_entry
|
@@ -115,7 +115,7 @@ class ACPService:
|
|
115
115
|
else:
|
116
116
|
raise ValueError("Either agent_name or agent_id must be provided")
|
117
117
|
|
118
|
-
task_message = TaskMessage.model_validate(json_rpc_response
|
118
|
+
task_message = TaskMessage.model_validate(json_rpc_response.result)
|
119
119
|
if span:
|
120
120
|
span.output = task_message.model_dump()
|
121
121
|
return task_message
|
@@ -147,7 +147,7 @@ class ACPService:
|
|
147
147
|
agent_name=agent_name,
|
148
148
|
method="event/send",
|
149
149
|
params={
|
150
|
-
"
|
150
|
+
"task_id": task_id,
|
151
151
|
"content": cast(TaskMessageContentParam, content.model_dump()),
|
152
152
|
},
|
153
153
|
)
|
@@ -163,7 +163,7 @@ class ACPService:
|
|
163
163
|
else:
|
164
164
|
raise ValueError("Either agent_name or agent_id must be provided")
|
165
165
|
|
166
|
-
event_entry = Event.model_validate(json_rpc_response
|
166
|
+
event_entry = Event.model_validate(json_rpc_response.result)
|
167
167
|
if span:
|
168
168
|
span.output = event_entry.model_dump()
|
169
169
|
return event_entry
|
@@ -204,7 +204,7 @@ class ACPService:
|
|
204
204
|
else:
|
205
205
|
raise ValueError("Either task_name or task_id must be provided")
|
206
206
|
|
207
|
-
task_entry = Task.model_validate(json_rpc_response
|
207
|
+
task_entry = Task.model_validate(json_rpc_response.result)
|
208
208
|
if span:
|
209
209
|
span.output = task_entry.model_dump()
|
210
210
|
return task_entry
|
@@ -24,6 +24,8 @@ from temporalio.worker import (
|
|
24
24
|
)
|
25
25
|
|
26
26
|
from agentex.lib.utils.logging import make_logger
|
27
|
+
from agentex.lib.utils.registration import register_agent
|
28
|
+
from agentex.lib.environment_variables import EnvironmentVariables
|
27
29
|
|
28
30
|
logger = make_logger(__name__)
|
29
31
|
|
@@ -103,9 +105,16 @@ class AgentexWorker:
|
|
103
105
|
workflow: type,
|
104
106
|
):
|
105
107
|
await self.start_health_check_server()
|
108
|
+
await self._register_agent()
|
106
109
|
temporal_client = await get_temporal_client(
|
107
110
|
temporal_address=os.environ.get("TEMPORAL_ADDRESS", "localhost:7233"),
|
108
111
|
)
|
112
|
+
|
113
|
+
# Enable debug mode if AgentEx debug is enabled (disables deadlock detection)
|
114
|
+
debug_enabled = os.environ.get("AGENTEX_DEBUG_ENABLED", "false").lower() == "true"
|
115
|
+
if debug_enabled:
|
116
|
+
logger.info("🐛 [WORKER] Temporal debug mode enabled - deadlock detection disabled")
|
117
|
+
|
109
118
|
worker = Worker(
|
110
119
|
client=temporal_client,
|
111
120
|
task_queue=self.task_queue,
|
@@ -115,6 +124,7 @@ class AgentexWorker:
|
|
115
124
|
workflow_runner=UnsandboxedWorkflowRunner(),
|
116
125
|
max_concurrent_activities=self.max_concurrent_activities,
|
117
126
|
build_id=str(uuid.uuid4()),
|
127
|
+
debug_mode=debug_enabled, # Disable deadlock detection in debug mode
|
118
128
|
)
|
119
129
|
|
120
130
|
logger.info(f"Starting workers for task queue: {self.task_queue}")
|
@@ -160,3 +170,17 @@ class AgentexWorker:
|
|
160
170
|
f"Failed to start health check server on alternative port {alt_port}: {e}"
|
161
171
|
)
|
162
172
|
raise
|
173
|
+
|
174
|
+
"""
|
175
|
+
Register the worker with the Agentex server.
|
176
|
+
|
177
|
+
Even though the Temporal server will also register the agent with the server,
|
178
|
+
doing this on the worker side is required to make sure that both share the API key
|
179
|
+
which is returned on registration and used to authenticate the worker with the Agentex server.
|
180
|
+
"""
|
181
|
+
async def _register_agent(self):
|
182
|
+
env_vars = EnvironmentVariables.refresh()
|
183
|
+
if env_vars and env_vars.AGENTEX_BASE_URL:
|
184
|
+
await register_agent(env_vars)
|
185
|
+
else:
|
186
|
+
logger.warning("AGENTEX_BASE_URL not set, skipping worker registration")
|
@@ -23,6 +23,7 @@ class EnvVarKeys(str, Enum):
|
|
23
23
|
AGENT_NAME = "AGENT_NAME"
|
24
24
|
AGENT_DESCRIPTION = "AGENT_DESCRIPTION"
|
25
25
|
AGENT_ID = "AGENT_ID"
|
26
|
+
AGENT_API_KEY = "AGENT_API_KEY"
|
26
27
|
# ACP Configuration
|
27
28
|
ACP_URL = "ACP_URL"
|
28
29
|
ACP_PORT = "ACP_PORT"
|
@@ -52,6 +53,7 @@ class EnvironmentVariables(BaseModel):
|
|
52
53
|
AGENT_NAME: str
|
53
54
|
AGENT_DESCRIPTION: str | None = None
|
54
55
|
AGENT_ID: str | None = None
|
56
|
+
AGENT_API_KEY: str | None = None
|
55
57
|
ACP_TYPE: str | None = "agentic"
|
56
58
|
# ACP Configuration
|
57
59
|
ACP_URL: str
|
@@ -1,15 +1,10 @@
|
|
1
1
|
import asyncio
|
2
|
-
import base64
|
3
2
|
import inspect
|
4
|
-
import json
|
5
|
-
import os
|
6
3
|
from collections.abc import AsyncGenerator, Awaitable, Callable
|
7
4
|
from contextlib import asynccontextmanager
|
8
5
|
from typing import Any
|
9
6
|
|
10
|
-
import httpx
|
11
7
|
import uvicorn
|
12
|
-
from agentex.lib.adk.utils._modules.client import create_async_agentex_client
|
13
8
|
from fastapi import FastAPI, Request
|
14
9
|
from fastapi.responses import StreamingResponse
|
15
10
|
from pydantic import TypeAdapter, ValidationError
|
@@ -30,6 +25,7 @@ from agentex.lib.types.task_message_updates import StreamTaskMessageFull, TaskMe
|
|
30
25
|
from agentex.types.task_message_content import TaskMessageContent
|
31
26
|
from agentex.lib.utils.logging import make_logger
|
32
27
|
from agentex.lib.utils.model_utils import BaseModel
|
28
|
+
from agentex.lib.utils.registration import register_agent
|
33
29
|
|
34
30
|
logger = make_logger(__name__)
|
35
31
|
|
@@ -74,7 +70,7 @@ class BaseACPServer(FastAPI):
|
|
74
70
|
async def lifespan_context(app: FastAPI):
|
75
71
|
env_vars = EnvironmentVariables.refresh()
|
76
72
|
if env_vars.AGENTEX_BASE_URL:
|
77
|
-
await
|
73
|
+
await register_agent(env_vars)
|
78
74
|
else:
|
79
75
|
logger.warning("AGENTEX_BASE_URL not set, skipping agent registration")
|
80
76
|
|
@@ -101,6 +97,16 @@ class BaseACPServer(FastAPI):
|
|
101
97
|
data = await request.json()
|
102
98
|
rpc_request = JSONRPCRequest(**data)
|
103
99
|
|
100
|
+
# Check if the request is authenticated
|
101
|
+
if refreshed_environment_variables and getattr(refreshed_environment_variables, "AGENT_API_KEY", None):
|
102
|
+
authorization_header = request.headers.get("x-agent-api-key")
|
103
|
+
if authorization_header != refreshed_environment_variables.AGENT_API_KEY:
|
104
|
+
return JSONRPCResponse(
|
105
|
+
id=rpc_request.id,
|
106
|
+
error=JSONRPCError(code=-32601, message="Unauthorized"),
|
107
|
+
)
|
108
|
+
|
109
|
+
|
104
110
|
# Check if method is valid first
|
105
111
|
try:
|
106
112
|
method = RPCMethod(rpc_request.method)
|
@@ -345,87 +351,4 @@ class BaseACPServer(FastAPI):
|
|
345
351
|
"""Start the Uvicorn server for async handlers."""
|
346
352
|
uvicorn.run(self, host=host, port=port, **kwargs)
|
347
353
|
|
348
|
-
|
349
|
-
if not env_vars.AUTH_PRINCIPAL_B64:
|
350
|
-
return None
|
351
|
-
|
352
|
-
try:
|
353
|
-
decoded_str = base64.b64decode(env_vars.AUTH_PRINCIPAL_B64).decode('utf-8')
|
354
|
-
return json.loads(decoded_str)
|
355
|
-
except Exception:
|
356
|
-
return None
|
357
|
-
|
358
|
-
async def _register_agent(self, env_vars: EnvironmentVariables):
|
359
|
-
"""Register this agent with the Agentex server"""
|
360
|
-
# Build the agent's own URL
|
361
|
-
full_acp_url = f"{env_vars.ACP_URL.rstrip('/')}:{env_vars.ACP_PORT}"
|
362
|
-
|
363
|
-
description = (
|
364
|
-
env_vars.AGENT_DESCRIPTION
|
365
|
-
or f"Generic description for agent: {env_vars.AGENT_NAME}"
|
366
|
-
)
|
367
|
-
|
368
|
-
# Prepare registration data
|
369
|
-
registration_data = {
|
370
|
-
"name": env_vars.AGENT_NAME,
|
371
|
-
"description": description,
|
372
|
-
"acp_url": full_acp_url,
|
373
|
-
"acp_type": env_vars.ACP_TYPE,
|
374
|
-
"principal_context": self._get_auth_principal(env_vars)
|
375
|
-
}
|
376
|
-
|
377
|
-
if env_vars.AGENT_ID:
|
378
|
-
registration_data["agent_id"] = env_vars.AGENT_ID
|
379
|
-
|
380
|
-
# Make the registration request
|
381
|
-
registration_url = f"{env_vars.AGENTEX_BASE_URL.rstrip('/')}/agents/register"
|
382
|
-
# Retry logic with configurable attempts and delay
|
383
|
-
max_retries = 3
|
384
|
-
base_delay = 5 # seconds
|
385
|
-
last_exception = None
|
386
|
-
|
387
|
-
attempt = 0
|
388
|
-
while attempt < max_retries:
|
389
|
-
try:
|
390
|
-
async with httpx.AsyncClient() as client:
|
391
|
-
response = await client.post(
|
392
|
-
registration_url, json=registration_data, timeout=30.0
|
393
|
-
)
|
394
|
-
if response.status_code == 200:
|
395
|
-
agent = response.json()
|
396
|
-
agent_id, agent_name = agent["id"], agent["name"]
|
397
|
-
|
398
|
-
os.environ["AGENT_ID"] = agent_id
|
399
|
-
os.environ["AGENT_NAME"] = agent_name
|
400
|
-
env_vars.AGENT_ID = agent_id
|
401
|
-
env_vars.AGENT_NAME = agent_name
|
402
|
-
global refreshed_environment_variables
|
403
|
-
refreshed_environment_variables = env_vars
|
404
|
-
logger.info(
|
405
|
-
f"Successfully registered agent '{env_vars.AGENT_NAME}' with Agentex server with acp_url: {full_acp_url}. Registration data: {registration_data}"
|
406
|
-
)
|
407
|
-
return # Success, exit the retry loop
|
408
|
-
else:
|
409
|
-
error_msg = f"Failed to register agent. Status: {response.status_code}, Response: {response.text}"
|
410
|
-
logger.error(error_msg)
|
411
|
-
last_exception = Exception(
|
412
|
-
f"Failed to startup agent: {response.text}"
|
413
|
-
)
|
414
|
-
|
415
|
-
except Exception as e:
|
416
|
-
logger.error(
|
417
|
-
f"Exception during agent registration attempt {attempt + 1}: {e}"
|
418
|
-
)
|
419
|
-
last_exception = e
|
420
|
-
attempt += 1
|
421
|
-
if attempt < max_retries:
|
422
|
-
delay = (attempt) * base_delay # 5, 10, 15 seconds
|
423
|
-
logger.info(
|
424
|
-
f"Retrying in {delay} seconds... (attempt {attempt}/{max_retries})"
|
425
|
-
)
|
426
|
-
await asyncio.sleep(delay)
|
427
|
-
|
428
|
-
# If we get here, all retries failed
|
429
|
-
raise last_exception or Exception(
|
430
|
-
f"Failed to register agent after {max_retries} attempts"
|
431
|
-
)
|
354
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
"""
|
2
|
+
Debug utilities for AgentEx development.
|
3
|
+
|
4
|
+
Provides debugging setup functionality that can be used across different components.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import debugpy # type: ignore
|
9
|
+
|
10
|
+
|
11
|
+
def setup_debug_if_enabled() -> None:
|
12
|
+
"""
|
13
|
+
Setup debugpy if debug mode is enabled via environment variables.
|
14
|
+
|
15
|
+
This function checks for AgentEx debug environment variables and configures
|
16
|
+
debugpy accordingly. It's designed to be called early in worker startup.
|
17
|
+
|
18
|
+
Environment Variables:
|
19
|
+
AGENTEX_DEBUG_ENABLED: Set to "true" to enable debug mode
|
20
|
+
AGENTEX_DEBUG_PORT: Port for debug server (default: 5678)
|
21
|
+
AGENTEX_DEBUG_TYPE: Type identifier for logging (default: "worker")
|
22
|
+
AGENTEX_DEBUG_WAIT_FOR_ATTACH: Set to "true" to wait for debugger attachment
|
23
|
+
|
24
|
+
Raises:
|
25
|
+
Any exception from debugpy setup (will bubble up naturally)
|
26
|
+
"""
|
27
|
+
if os.getenv("AGENTEX_DEBUG_ENABLED") == "true":
|
28
|
+
debug_port = int(os.getenv("AGENTEX_DEBUG_PORT", "5678"))
|
29
|
+
debug_type = os.getenv("AGENTEX_DEBUG_TYPE", "worker")
|
30
|
+
wait_for_attach = os.getenv("AGENTEX_DEBUG_WAIT_FOR_ATTACH", "false").lower() == "true"
|
31
|
+
|
32
|
+
# Configure debugpy
|
33
|
+
debugpy.configure(subProcess=False)
|
34
|
+
debugpy.listen(debug_port)
|
35
|
+
|
36
|
+
print(f"🐛 [{debug_type.upper()}] Debug server listening on port {debug_port}")
|
37
|
+
|
38
|
+
if wait_for_attach:
|
39
|
+
print(f"⏳ [{debug_type.upper()}] Waiting for debugger to attach...")
|
40
|
+
debugpy.wait_for_client()
|
41
|
+
print(f"✅ [{debug_type.upper()}] Debugger attached!")
|
42
|
+
else:
|
43
|
+
print(f"📡 [{debug_type.upper()}] Ready for debugger attachment")
|
44
|
+
|
45
|
+
|
46
|
+
def is_debug_enabled() -> bool:
|
47
|
+
"""
|
48
|
+
Check if debug mode is currently enabled.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
bool: True if AGENTEX_DEBUG_ENABLED is set to "true"
|
52
|
+
"""
|
53
|
+
return os.getenv("AGENTEX_DEBUG_ENABLED", "false").lower() == "true"
|
54
|
+
|
55
|
+
|
56
|
+
def get_debug_port() -> int:
|
57
|
+
"""
|
58
|
+
Get the debug port from environment variables.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
int: Debug port (default: 5678)
|
62
|
+
"""
|
63
|
+
return int(os.getenv("AGENTEX_DEBUG_PORT", "5678"))
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import base64
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import httpx
|
5
|
+
import asyncio
|
6
|
+
|
7
|
+
from agentex.lib.environment_variables import EnvironmentVariables, refreshed_environment_variables
|
8
|
+
from agentex.lib.utils.logging import make_logger
|
9
|
+
|
10
|
+
logger = make_logger(__name__)
|
11
|
+
|
12
|
+
def get_auth_principal(env_vars: EnvironmentVariables):
|
13
|
+
if not env_vars.AUTH_PRINCIPAL_B64:
|
14
|
+
return None
|
15
|
+
|
16
|
+
try:
|
17
|
+
decoded_str = base64.b64decode(env_vars.AUTH_PRINCIPAL_B64).decode('utf-8')
|
18
|
+
return json.loads(decoded_str)
|
19
|
+
except Exception:
|
20
|
+
return None
|
21
|
+
|
22
|
+
async def register_agent(env_vars: EnvironmentVariables):
|
23
|
+
"""Register this agent with the Agentex server"""
|
24
|
+
if not env_vars.AGENTEX_BASE_URL:
|
25
|
+
logger.warning("AGENTEX_BASE_URL is not set, skipping registration")
|
26
|
+
return
|
27
|
+
# Build the agent's own URL
|
28
|
+
full_acp_url = f"{env_vars.ACP_URL.rstrip('/')}:{env_vars.ACP_PORT}"
|
29
|
+
|
30
|
+
description = (
|
31
|
+
env_vars.AGENT_DESCRIPTION
|
32
|
+
or f"Generic description for agent: {env_vars.AGENT_NAME}"
|
33
|
+
)
|
34
|
+
|
35
|
+
# Prepare registration data
|
36
|
+
registration_data = {
|
37
|
+
"name": env_vars.AGENT_NAME,
|
38
|
+
"description": description,
|
39
|
+
"acp_url": full_acp_url,
|
40
|
+
"acp_type": env_vars.ACP_TYPE,
|
41
|
+
"principal_context": get_auth_principal(env_vars)
|
42
|
+
}
|
43
|
+
|
44
|
+
if env_vars.AGENT_ID:
|
45
|
+
registration_data["agent_id"] = env_vars.AGENT_ID
|
46
|
+
|
47
|
+
# Make the registration request
|
48
|
+
registration_url = f"{env_vars.AGENTEX_BASE_URL.rstrip('/')}/agents/register"
|
49
|
+
# Retry logic with configurable attempts and delay
|
50
|
+
max_retries = 3
|
51
|
+
base_delay = 5 # seconds
|
52
|
+
last_exception = None
|
53
|
+
|
54
|
+
attempt = 0
|
55
|
+
while attempt < max_retries:
|
56
|
+
try:
|
57
|
+
async with httpx.AsyncClient() as client:
|
58
|
+
response = await client.post(
|
59
|
+
registration_url, json=registration_data, timeout=30.0
|
60
|
+
)
|
61
|
+
if response.status_code == 200:
|
62
|
+
agent = response.json()
|
63
|
+
agent_id, agent_name = agent["id"], agent["name"]
|
64
|
+
agent_api_key = agent["agent_api_key"]
|
65
|
+
|
66
|
+
os.environ["AGENT_ID"] = agent_id
|
67
|
+
os.environ["AGENT_NAME"] = agent_name
|
68
|
+
os.environ["AGENT_API_KEY"] = agent_api_key
|
69
|
+
env_vars.AGENT_ID = agent_id
|
70
|
+
env_vars.AGENT_NAME = agent_name
|
71
|
+
env_vars.AGENT_API_KEY = agent_api_key
|
72
|
+
global refreshed_environment_variables
|
73
|
+
refreshed_environment_variables = env_vars
|
74
|
+
logger.info(
|
75
|
+
f"Successfully registered agent '{env_vars.AGENT_NAME}' with Agentex server with acp_url: {full_acp_url}. Registration data: {registration_data}"
|
76
|
+
)
|
77
|
+
return # Success, exit the retry loop
|
78
|
+
else:
|
79
|
+
error_msg = f"Failed to register agent. Status: {response.status_code}, Response: {response.text}"
|
80
|
+
logger.error(error_msg)
|
81
|
+
last_exception = Exception(
|
82
|
+
f"Failed to startup agent: {response.text}"
|
83
|
+
)
|
84
|
+
|
85
|
+
except Exception as e:
|
86
|
+
logger.error(
|
87
|
+
f"Exception during agent registration attempt {attempt + 1}: {e}"
|
88
|
+
)
|
89
|
+
last_exception = e
|
90
|
+
attempt += 1
|
91
|
+
if attempt < max_retries:
|
92
|
+
delay = (attempt) * base_delay # 5, 10, 15 seconds
|
93
|
+
logger.info(
|
94
|
+
f"Retrying in {delay} seconds... (attempt {attempt}/{max_retries})"
|
95
|
+
)
|
96
|
+
await asyncio.sleep(delay)
|
97
|
+
|
98
|
+
# If we get here, all retries failed
|
99
|
+
raise last_exception or Exception(
|
100
|
+
f"Failed to register agent after {max_retries} attempts"
|
101
|
+
)
|
agentex/resources/agents.py
CHANGED
@@ -508,17 +508,24 @@ class AgentsResource(SyncAPIResource):
|
|
508
508
|
raise ValueError("Either agent_id or agent_name must be provided")
|
509
509
|
|
510
510
|
with raw_agent_rpc_response as response:
|
511
|
-
for
|
512
|
-
if
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
511
|
+
for _line in response.iter_lines():
|
512
|
+
if not _line:
|
513
|
+
continue
|
514
|
+
line = _line.strip()
|
515
|
+
# Handle optional SSE-style prefix
|
516
|
+
if line.startswith("data:"):
|
517
|
+
line = line[len("data:"):].strip()
|
518
|
+
if not line:
|
519
|
+
continue
|
520
|
+
try:
|
521
|
+
chunk_rpc_response = SendMessageStreamResponse.model_validate(
|
522
|
+
json.loads(line),
|
523
|
+
from_attributes=True
|
524
|
+
)
|
525
|
+
yield chunk_rpc_response
|
526
|
+
except json.JSONDecodeError:
|
527
|
+
# Skip invalid JSON lines
|
528
|
+
continue
|
522
529
|
|
523
530
|
def send_event(
|
524
531
|
self,
|
@@ -1048,17 +1055,24 @@ class AsyncAgentsResource(AsyncAPIResource):
|
|
1048
1055
|
raise ValueError("Either agent_id or agent_name must be provided")
|
1049
1056
|
|
1050
1057
|
async with raw_agent_rpc_response as response:
|
1051
|
-
async for
|
1052
|
-
if
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1058
|
+
async for _line in response.iter_lines():
|
1059
|
+
if not _line:
|
1060
|
+
continue
|
1061
|
+
line = _line.strip()
|
1062
|
+
# Handle optional SSE-style prefix
|
1063
|
+
if line.startswith("data:"):
|
1064
|
+
line = line[len("data:"):].strip()
|
1065
|
+
if not line:
|
1066
|
+
continue
|
1067
|
+
try:
|
1068
|
+
chunk_rpc_response = SendMessageStreamResponse.model_validate(
|
1069
|
+
json.loads(line),
|
1070
|
+
from_attributes=True
|
1071
|
+
)
|
1072
|
+
yield chunk_rpc_response
|
1073
|
+
except json.JSONDecodeError:
|
1074
|
+
# Skip invalid JSON lines
|
1075
|
+
continue
|
1062
1076
|
|
1063
1077
|
async def send_event(
|
1064
1078
|
self,
|
agentex/types/agent.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
2
2
|
|
3
3
|
from typing import Optional
|
4
|
+
from datetime import datetime
|
4
5
|
from typing_extensions import Literal
|
5
6
|
|
6
7
|
from .._models import BaseModel
|
@@ -16,12 +17,18 @@ class Agent(BaseModel):
|
|
16
17
|
acp_type: AcpType
|
17
18
|
"""The type of the ACP Server (Either sync or agentic)"""
|
18
19
|
|
20
|
+
created_at: datetime
|
21
|
+
"""The timestamp when the agent was created"""
|
22
|
+
|
19
23
|
description: str
|
20
24
|
"""The description of the action."""
|
21
25
|
|
22
26
|
name: str
|
23
27
|
"""The unique name of the agent."""
|
24
28
|
|
29
|
+
updated_at: datetime
|
30
|
+
"""The timestamp when the agent was last updated"""
|
31
|
+
|
25
32
|
status: Optional[Literal["Pending", "Building", "Ready", "Failed", "Unknown"]] = None
|
26
33
|
"""The status of the action, indicating if it's building, ready, failed, etc."""
|
27
34
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: agentex-sdk
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.8
|
4
4
|
Summary: The official Python library for the agentex API
|
5
5
|
Project-URL: Homepage, https://github.com/scaleapi/agentex-python
|
6
6
|
Project-URL: Repository, https://github.com/scaleapi/agentex-python
|
@@ -126,6 +126,37 @@ asyncio.run(main())
|
|
126
126
|
|
127
127
|
Functionality between the synchronous and asynchronous clients is otherwise identical.
|
128
128
|
|
129
|
+
## Debugging
|
130
|
+
|
131
|
+
AgentEx provides built-in debugging support for **temporal projects** during local development.
|
132
|
+
|
133
|
+
```bash
|
134
|
+
# Basic debugging
|
135
|
+
uv run agentex agents run --manifest manifest.yaml --debug-worker
|
136
|
+
|
137
|
+
# Wait for debugger to attach before starting
|
138
|
+
uv run agentex agents run --manifest manifest.yaml --debug-worker --wait-for-debugger
|
139
|
+
|
140
|
+
# Custom debug port
|
141
|
+
uv run agentex agents run --manifest manifest.yaml --debug-worker --debug-port 5679
|
142
|
+
```
|
143
|
+
|
144
|
+
For **VS Code**, add this configuration to `.vscode/launch.json`:
|
145
|
+
|
146
|
+
```json
|
147
|
+
{
|
148
|
+
"name": "Attach to AgentEx Worker",
|
149
|
+
"type": "debugpy",
|
150
|
+
"request": "attach",
|
151
|
+
"connect": { "host": "localhost", "port": 5678 },
|
152
|
+
"pathMappings": [{ "localRoot": "${workspaceFolder}", "remoteRoot": "." }],
|
153
|
+
"justMyCode": false,
|
154
|
+
"console": "integratedTerminal"
|
155
|
+
}
|
156
|
+
```
|
157
|
+
|
158
|
+
The debug server automatically finds an available port starting from 5678 and prints connection details when starting.
|
159
|
+
|
129
160
|
### With aiohttp
|
130
161
|
|
131
162
|
By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend.
|