agentex-sdk 0.2.5__py3-none-any.whl → 0.2.7__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 (47) hide show
  1. agentex/_base_client.py +4 -1
  2. agentex/_files.py +4 -4
  3. agentex/_version.py +1 -1
  4. agentex/lib/adk/_modules/acp.py +2 -2
  5. agentex/lib/adk/_modules/agent_task_tracker.py +2 -2
  6. agentex/lib/adk/_modules/agents.py +2 -2
  7. agentex/lib/adk/_modules/events.py +2 -2
  8. agentex/lib/adk/_modules/messages.py +2 -2
  9. agentex/lib/adk/_modules/state.py +2 -2
  10. agentex/lib/adk/_modules/streaming.py +2 -2
  11. agentex/lib/adk/_modules/tasks.py +2 -2
  12. agentex/lib/adk/_modules/tracing.py +2 -2
  13. agentex/lib/adk/providers/_modules/litellm.py +2 -1
  14. agentex/lib/adk/providers/_modules/openai.py +2 -1
  15. agentex/lib/adk/providers/_modules/sgp.py +2 -1
  16. agentex/lib/adk/utils/_modules/client.py +21 -35
  17. agentex/lib/adk/utils/_modules/templating.py +2 -1
  18. agentex/lib/cli/commands/agents.py +36 -2
  19. agentex/lib/cli/debug/__init__.py +15 -0
  20. agentex/lib/cli/debug/debug_config.py +116 -0
  21. agentex/lib/cli/debug/debug_handlers.py +174 -0
  22. agentex/lib/cli/handlers/agent_handlers.py +3 -2
  23. agentex/lib/cli/handlers/deploy_handlers.py +1 -2
  24. agentex/lib/cli/handlers/run_handlers.py +24 -7
  25. agentex/lib/cli/templates/default/pyproject.toml.j2 +0 -1
  26. agentex/lib/cli/templates/sync/pyproject.toml.j2 +0 -1
  27. agentex/lib/cli/templates/temporal/project/acp.py.j2 +31 -0
  28. agentex/lib/cli/templates/temporal/project/run_worker.py.j2 +4 -1
  29. agentex/lib/cli/templates/temporal/pyproject.toml.j2 +1 -1
  30. agentex/lib/core/services/adk/acp/acp.py +5 -5
  31. agentex/lib/core/temporal/activities/__init__.py +2 -1
  32. agentex/lib/core/temporal/workers/worker.py +24 -0
  33. agentex/lib/core/tracing/processors/agentex_tracing_processor.py +2 -1
  34. agentex/lib/environment_variables.py +7 -1
  35. agentex/lib/sdk/fastacp/base/base_acp_server.py +13 -89
  36. agentex/lib/sdk/fastacp/impl/agentic_base_acp.py +3 -1
  37. agentex/lib/utils/debug.py +63 -0
  38. agentex/lib/utils/registration.py +101 -0
  39. agentex/resources/tasks.py +54 -4
  40. agentex/types/__init__.py +1 -0
  41. agentex/types/agent.py +7 -0
  42. agentex/types/task_list_params.py +14 -0
  43. {agentex_sdk-0.2.5.dist-info → agentex_sdk-0.2.7.dist-info}/METADATA +32 -1
  44. {agentex_sdk-0.2.5.dist-info → agentex_sdk-0.2.7.dist-info}/RECORD +47 -41
  45. {agentex_sdk-0.2.5.dist-info → agentex_sdk-0.2.7.dist-info}/WHEEL +0 -0
  46. {agentex_sdk-0.2.5.dist-info → agentex_sdk-0.2.7.dist-info}/entry_points.txt +0 -0
  47. {agentex_sdk-0.2.5.dist-info → agentex_sdk-0.2.7.dist-info}/licenses/LICENSE +0 -0
agentex/_base_client.py CHANGED
@@ -532,7 +532,10 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
532
532
  is_body_allowed = options.method.lower() != "get"
533
533
 
534
534
  if is_body_allowed:
535
- kwargs["json"] = json_data if is_given(json_data) else None
535
+ if isinstance(json_data, bytes):
536
+ kwargs["content"] = json_data
537
+ else:
538
+ kwargs["json"] = json_data if is_given(json_data) else None
536
539
  kwargs["files"] = files
537
540
  else:
538
541
  headers.pop("Content-Type", None)
agentex/_files.py CHANGED
@@ -69,12 +69,12 @@ def _transform_file(file: FileTypes) -> HttpxFileTypes:
69
69
  return file
70
70
 
71
71
  if is_tuple_t(file):
72
- return (file[0], _read_file_content(file[1]), *file[2:])
72
+ return (file[0], read_file_content(file[1]), *file[2:])
73
73
 
74
74
  raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
75
75
 
76
76
 
77
- def _read_file_content(file: FileContent) -> HttpxFileContent:
77
+ def read_file_content(file: FileContent) -> HttpxFileContent:
78
78
  if isinstance(file, os.PathLike):
79
79
  return pathlib.Path(file).read_bytes()
80
80
  return file
@@ -111,12 +111,12 @@ async def _async_transform_file(file: FileTypes) -> HttpxFileTypes:
111
111
  return file
112
112
 
113
113
  if is_tuple_t(file):
114
- return (file[0], await _async_read_file_content(file[1]), *file[2:])
114
+ return (file[0], await async_read_file_content(file[1]), *file[2:])
115
115
 
116
116
  raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")
117
117
 
118
118
 
119
- async def _async_read_file_content(file: FileContent) -> HttpxFileContent:
119
+ async def async_read_file_content(file: FileContent) -> HttpxFileContent:
120
120
  if isinstance(file, os.PathLike):
121
121
  return await anyio.Path(file).read_bytes()
122
122
 
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.5" # x-release-please-version
4
+ __version__ = "0.2.7" # x-release-please-version
@@ -4,7 +4,7 @@ from typing import Any
4
4
  from temporalio.common import RetryPolicy
5
5
 
6
6
  from agentex import AsyncAgentex
7
- from agentex.lib.adk.utils._modules.client import get_async_agentex_client
7
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
8
8
  from agentex.lib.core.services.adk.acp.acp import ACPService
9
9
  from agentex.lib.core.temporal.activities.activity_helpers import ActivityHelpers
10
10
  from agentex.lib.core.temporal.activities.adk.acp.acp_activities import (
@@ -41,7 +41,7 @@ class ACPModule:
41
41
  acp_activities (Optional[ACPActivities]): Optional pre-configured ACP activities. If None, will be auto-initialized.
42
42
  """
43
43
  if acp_service is None:
44
- agentex_client = get_async_agentex_client()
44
+ agentex_client = create_async_agentex_client()
45
45
  tracer = AsyncTracer(agentex_client)
46
46
  self._acp_service = ACPService(agentex_client=agentex_client, tracer=tracer)
47
47
  else:
@@ -3,7 +3,7 @@ from datetime import timedelta
3
3
  from temporalio.common import RetryPolicy
4
4
 
5
5
  from agentex import AsyncAgentex
6
- from agentex.lib.adk.utils._modules.client import get_async_agentex_client
6
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
7
7
  from agentex.lib.core.services.adk.agent_task_tracker import AgentTaskTrackerService
8
8
  from agentex.lib.core.temporal.activities.activity_helpers import ActivityHelpers
9
9
  from agentex.lib.core.temporal.activities.adk.agent_task_tracker_activities import (
@@ -34,7 +34,7 @@ class AgentTaskTrackerModule:
34
34
  agent_task_tracker_service: AgentTaskTrackerService | None = None,
35
35
  ):
36
36
  if agent_task_tracker_service is None:
37
- agentex_client = get_async_agentex_client()
37
+ agentex_client = create_async_agentex_client()
38
38
  tracer = AsyncTracer(agentex_client)
39
39
  self._agent_task_tracker_service = AgentTaskTrackerService(
40
40
  agentex_client=agentex_client, tracer=tracer
@@ -1,7 +1,7 @@
1
1
  from datetime import timedelta
2
2
  from typing import Optional
3
3
 
4
- from agentex.lib.adk.utils._modules.client import get_async_agentex_client
4
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
5
5
  from agentex.lib.core.temporal.activities.adk.agents_activities import AgentsActivityName, GetAgentParams
6
6
  from temporalio.common import RetryPolicy
7
7
 
@@ -29,7 +29,7 @@ class AgentsModule:
29
29
  agents_service: Optional[AgentsService] = None,
30
30
  ):
31
31
  if agents_service is None:
32
- agentex_client = get_async_agentex_client()
32
+ agentex_client = create_async_agentex_client()
33
33
  tracer = AsyncTracer(agentex_client)
34
34
  self._agents_service = AgentsService(agentex_client=agentex_client, tracer=tracer)
35
35
  else:
@@ -3,7 +3,7 @@ from datetime import timedelta
3
3
  from temporalio.common import RetryPolicy
4
4
 
5
5
  from agentex import AsyncAgentex
6
- from agentex.lib.adk.utils._modules.client import get_async_agentex_client
6
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
7
7
  from agentex.lib.core.services.adk.events import EventsService
8
8
  from agentex.lib.core.temporal.activities.activity_helpers import ActivityHelpers
9
9
  from agentex.lib.core.temporal.activities.adk.events_activities import (
@@ -33,7 +33,7 @@ class EventsModule:
33
33
  events_service: EventsService | None = None,
34
34
  ):
35
35
  if events_service is None:
36
- agentex_client = get_async_agentex_client()
36
+ agentex_client = create_async_agentex_client()
37
37
  tracer = AsyncTracer(agentex_client)
38
38
  self._events_service = EventsService(
39
39
  agentex_client=agentex_client, tracer=tracer
@@ -3,7 +3,7 @@ from datetime import timedelta
3
3
  from temporalio.common import RetryPolicy
4
4
 
5
5
  from agentex import AsyncAgentex
6
- from agentex.lib.adk.utils._modules.client import get_async_agentex_client
6
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
7
7
  from agentex.lib.core.adapters.streams.adapter_redis import RedisStreamRepository
8
8
  from agentex.lib.core.services.adk.messages import MessagesService
9
9
  from agentex.lib.core.services.adk.streaming import StreamingService
@@ -38,7 +38,7 @@ class MessagesModule:
38
38
  messages_service: MessagesService | None = None,
39
39
  ):
40
40
  if messages_service is None:
41
- agentex_client = get_async_agentex_client()
41
+ agentex_client = create_async_agentex_client()
42
42
  stream_repository = RedisStreamRepository()
43
43
  streaming_service = StreamingService(
44
44
  agentex_client=agentex_client,
@@ -5,7 +5,7 @@ from pydantic import BaseModel
5
5
  from temporalio.common import RetryPolicy
6
6
 
7
7
  from agentex import AsyncAgentex
8
- from agentex.lib.adk.utils._modules.client import get_async_agentex_client
8
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
9
9
  from agentex.lib.core.services.adk.state import StateService
10
10
  from agentex.lib.core.temporal.activities.activity_helpers import ActivityHelpers
11
11
  from agentex.lib.core.temporal.activities.adk.state_activities import (
@@ -37,7 +37,7 @@ class StateModule:
37
37
  state_service: StateService | None = None,
38
38
  ):
39
39
  if state_service is None:
40
- agentex_client = get_async_agentex_client()
40
+ agentex_client = create_async_agentex_client()
41
41
  tracer = AsyncTracer(agentex_client)
42
42
  self._state_service = StateService(
43
43
  agentex_client=agentex_client, tracer=tracer
@@ -1,7 +1,7 @@
1
1
  from temporalio.common import RetryPolicy
2
2
 
3
3
  from agentex import AsyncAgentex
4
- from agentex.lib.adk.utils._modules.client import get_async_agentex_client
4
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
5
5
  from agentex.lib.core.adapters.streams.adapter_redis import RedisStreamRepository
6
6
  from agentex.lib.core.services.adk.streaming import (
7
7
  StreamingService,
@@ -35,7 +35,7 @@ class StreamingModule:
35
35
  """
36
36
  if streaming_service is None:
37
37
  stream_repository = RedisStreamRepository()
38
- agentex_client = get_async_agentex_client()
38
+ agentex_client = create_async_agentex_client()
39
39
  self._streaming_service = StreamingService(
40
40
  agentex_client=agentex_client,
41
41
  stream_repository=stream_repository,
@@ -3,7 +3,7 @@ from datetime import timedelta
3
3
  from temporalio.common import RetryPolicy
4
4
 
5
5
  from agentex import AsyncAgentex
6
- from agentex.lib.adk.utils._modules.client import get_async_agentex_client
6
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
7
7
  from agentex.lib.core.services.adk.tasks import TasksService
8
8
  from agentex.lib.core.temporal.activities.activity_helpers import ActivityHelpers
9
9
  from agentex.lib.core.temporal.activities.adk.tasks_activities import (
@@ -32,7 +32,7 @@ class TasksModule:
32
32
  tasks_service: TasksService | None = None,
33
33
  ):
34
34
  if tasks_service is None:
35
- agentex_client = get_async_agentex_client()
35
+ agentex_client = create_async_agentex_client()
36
36
  tracer = AsyncTracer(agentex_client)
37
37
  self._tasks_service = TasksService(
38
38
  agentex_client=agentex_client, tracer=tracer
@@ -6,7 +6,7 @@ from typing import Any
6
6
  from temporalio.common import RetryPolicy
7
7
 
8
8
  from agentex import AsyncAgentex
9
- from agentex.lib.adk.utils._modules.client import get_async_agentex_client
9
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
10
10
  from agentex.lib.core.services.adk.tracing import TracingService
11
11
  from agentex.lib.core.temporal.activities.activity_helpers import ActivityHelpers
12
12
  from agentex.lib.core.temporal.activities.adk.tracing_activities import (
@@ -39,7 +39,7 @@ class TracingModule:
39
39
  tracing_activities (Optional[TracingActivities]): Optional pre-configured tracing activities. If None, will be auto-initialized.
40
40
  """
41
41
  if tracing_service is None:
42
- agentex_client = get_async_agentex_client()
42
+ agentex_client = create_async_agentex_client()
43
43
  tracer = AsyncTracer(agentex_client)
44
44
  self._tracing_service = TracingService(tracer=tracer)
45
45
  else:
@@ -1,6 +1,7 @@
1
1
  from collections.abc import AsyncGenerator
2
2
  from datetime import timedelta
3
3
 
4
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
4
5
  from temporalio.common import RetryPolicy
5
6
 
6
7
  from agentex import AsyncAgentex
@@ -39,7 +40,7 @@ class LiteLLMModule:
39
40
  ):
40
41
  if litellm_service is None:
41
42
  # Create default service
42
- agentex_client = AsyncAgentex()
43
+ agentex_client = create_async_agentex_client()
43
44
  stream_repository = RedisStreamRepository()
44
45
  streaming_service = StreamingService(
45
46
  agentex_client=agentex_client,
@@ -1,6 +1,7 @@
1
1
  from datetime import timedelta
2
2
  from typing import Any, Literal
3
3
 
4
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
4
5
  from agents import Agent, RunResult, RunResultStreaming
5
6
  from agents.agent import StopAtTools, ToolsToFinalOutputFunction
6
7
  from agents.agent_output import AgentOutputSchemaBase
@@ -46,7 +47,7 @@ class OpenAIModule:
46
47
  ):
47
48
  if openai_service is None:
48
49
  # Create default service
49
- agentex_client = AsyncAgentex()
50
+ agentex_client = create_async_agentex_client()
50
51
  stream_repository = RedisStreamRepository()
51
52
  streaming_service = StreamingService(
52
53
  agentex_client=agentex_client,
@@ -1,5 +1,6 @@
1
1
  from datetime import timedelta
2
2
 
3
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
3
4
  from scale_gp import SGPClient, SGPClientError
4
5
  from temporalio.common import RetryPolicy
5
6
 
@@ -33,7 +34,7 @@ class SGPModule:
33
34
  if sgp_service is None:
34
35
  try:
35
36
  sgp_client = SGPClient()
36
- agentex_client = AsyncAgentex()
37
+ agentex_client = create_async_agentex_client()
37
38
  tracer = AsyncTracer(agentex_client)
38
39
  self._sgp_service = SGPService(sgp_client=sgp_client, tracer=tracer)
39
40
  except SGPClientError:
@@ -1,43 +1,29 @@
1
- import threading
2
- from typing import Dict, Optional, Any
1
+ import httpx
3
2
 
4
3
  from agentex import AsyncAgentex
5
- from agentex.lib.environment_variables import EnvironmentVariables, refreshed_environment_variables
4
+ from agentex.lib.environment_variables import EnvironmentVariables
5
+ from agentex.lib.utils.logging import make_logger
6
6
 
7
- _client: Optional["AsyncAgentex"] = None
8
- _cached_headers: Dict[str, str] = {}
9
- _init_kwargs: Dict[str, Any] = {}
10
- _lock = threading.RLock()
7
+ logger = make_logger(__name__)
11
8
 
12
9
 
13
- def _build_headers() -> Dict[str, str]:
14
- EnvironmentVariables.refresh()
15
- if refreshed_environment_variables and getattr(refreshed_environment_variables, "AGENT_ID", None):
16
- return {"x-agent-identity": refreshed_environment_variables.AGENT_ID}
17
- return {}
10
+ class EnvAuth(httpx.Auth):
11
+ def __init__(self, header_name="x-agent-api-key"):
12
+ self.header_name = header_name
18
13
 
14
+ def auth_flow(self, request):
15
+ # This gets called for every request
16
+ env_vars = EnvironmentVariables.refresh()
17
+ if env_vars:
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}")
23
+ yield request
19
24
 
20
- def get_async_agentex_client(**kwargs) -> "AsyncAgentex":
21
- """
22
- Return a cached AsyncAgentex instance (created synchronously).
23
- Each call re-checks env vars and updates client.default_headers if needed.
24
- """
25
- global _client, _cached_headers, _init_kwargs
26
25
 
27
- new_headers = _build_headers()
28
-
29
- with _lock:
30
- # First time (or kwargs changed) -> build a new client
31
- if _client is None or kwargs != _init_kwargs:
32
- _client = AsyncAgentex(default_headers=new_headers.copy(), **kwargs)
33
- _cached_headers = new_headers
34
- _init_kwargs = dict(kwargs)
35
- return _client
36
-
37
- # Same client; maybe headers changed
38
- if new_headers != _cached_headers:
39
- _cached_headers = new_headers
40
- _client.default_headers.clear()
41
- _client.default_headers.update(new_headers)
42
-
43
- return _client
26
+ def create_async_agentex_client(**kwargs) -> AsyncAgentex:
27
+ client = AsyncAgentex(**kwargs)
28
+ client._client.auth = EnvAuth()
29
+ return client
@@ -1,6 +1,7 @@
1
1
  from datetime import timedelta
2
2
  from typing import Any
3
3
 
4
+ from agentex.lib.adk.utils._modules.client import create_async_agentex_client
4
5
  from temporalio.common import RetryPolicy
5
6
 
6
7
  from agentex import AsyncAgentex
@@ -39,7 +40,7 @@ class TemplatingModule:
39
40
  templating_service (Optional[TemplatingService]): Optional pre-configured templating service. If None, will be auto-initialized.
40
41
  """
41
42
  if templating_service is None:
42
- agentex_client = AsyncAgentex()
43
+ agentex_client = create_async_agentex_client()
43
44
  tracer = AsyncTracer(agentex_client)
44
45
  self._templating_service = TemplatingService(tracer=tracer)
45
46
  else:
@@ -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