agentex-sdk 0.2.3__py3-none-any.whl → 0.2.5__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 (34) hide show
  1. agentex/_version.py +1 -1
  2. agentex/lib/adk/_modules/acp.py +2 -1
  3. agentex/lib/adk/_modules/agent_task_tracker.py +2 -1
  4. agentex/lib/adk/_modules/agents.py +2 -1
  5. agentex/lib/adk/_modules/events.py +2 -1
  6. agentex/lib/adk/_modules/messages.py +2 -1
  7. agentex/lib/adk/_modules/state.py +2 -1
  8. agentex/lib/adk/_modules/streaming.py +2 -1
  9. agentex/lib/adk/_modules/tasks.py +2 -1
  10. agentex/lib/adk/_modules/tracing.py +2 -1
  11. agentex/lib/adk/utils/_modules/client.py +43 -0
  12. agentex/lib/cli/commands/agents.py +3 -3
  13. agentex/lib/cli/handlers/agent_handlers.py +1 -1
  14. agentex/lib/cli/handlers/cleanup_handlers.py +9 -15
  15. agentex/lib/cli/handlers/deploy_handlers.py +28 -4
  16. agentex/lib/cli/handlers/run_handlers.py +19 -93
  17. agentex/lib/cli/templates/sync/project/acp.py.j2 +15 -64
  18. agentex/lib/cli/utils/path_utils.py +143 -0
  19. agentex/lib/sdk/fastacp/base/base_acp_server.py +11 -1
  20. agentex/lib/types/converters.py +60 -0
  21. agentex/resources/agents.py +9 -8
  22. agentex/resources/messages/messages.py +4 -0
  23. agentex/resources/tasks.py +9 -10
  24. agentex/types/__init__.py +1 -2
  25. agentex/types/message_list_params.py +1 -0
  26. agentex/types/shared/__init__.py +3 -0
  27. agentex/types/shared/delete_response.py +11 -0
  28. {agentex_sdk-0.2.3.dist-info → agentex_sdk-0.2.5.dist-info}/METADATA +2 -2
  29. {agentex_sdk-0.2.3.dist-info → agentex_sdk-0.2.5.dist-info}/RECORD +32 -29
  30. agentex/types/task_delete_by_name_response.py +0 -8
  31. agentex/types/task_delete_response.py +0 -8
  32. {agentex_sdk-0.2.3.dist-info → agentex_sdk-0.2.5.dist-info}/WHEEL +0 -0
  33. {agentex_sdk-0.2.3.dist-info → agentex_sdk-0.2.5.dist-info}/entry_points.txt +0 -0
  34. {agentex_sdk-0.2.3.dist-info → agentex_sdk-0.2.5.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.3" # x-release-please-version
4
+ __version__ = "0.2.5" # x-release-please-version
@@ -4,6 +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
8
  from agentex.lib.core.services.adk.acp.acp import ACPService
8
9
  from agentex.lib.core.temporal.activities.activity_helpers import ActivityHelpers
9
10
  from agentex.lib.core.temporal.activities.adk.acp.acp_activities import (
@@ -40,7 +41,7 @@ class ACPModule:
40
41
  acp_activities (Optional[ACPActivities]): Optional pre-configured ACP activities. If None, will be auto-initialized.
41
42
  """
42
43
  if acp_service is None:
43
- agentex_client = AsyncAgentex()
44
+ agentex_client = get_async_agentex_client()
44
45
  tracer = AsyncTracer(agentex_client)
45
46
  self._acp_service = ACPService(agentex_client=agentex_client, tracer=tracer)
46
47
  else:
@@ -3,6 +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
7
  from agentex.lib.core.services.adk.agent_task_tracker import AgentTaskTrackerService
7
8
  from agentex.lib.core.temporal.activities.activity_helpers import ActivityHelpers
8
9
  from agentex.lib.core.temporal.activities.adk.agent_task_tracker_activities import (
@@ -33,7 +34,7 @@ class AgentTaskTrackerModule:
33
34
  agent_task_tracker_service: AgentTaskTrackerService | None = None,
34
35
  ):
35
36
  if agent_task_tracker_service is None:
36
- agentex_client = AsyncAgentex()
37
+ agentex_client = get_async_agentex_client()
37
38
  tracer = AsyncTracer(agentex_client)
38
39
  self._agent_task_tracker_service = AgentTaskTrackerService(
39
40
  agentex_client=agentex_client, tracer=tracer
@@ -1,6 +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
5
  from agentex.lib.core.temporal.activities.adk.agents_activities import AgentsActivityName, GetAgentParams
5
6
  from temporalio.common import RetryPolicy
6
7
 
@@ -28,7 +29,7 @@ class AgentsModule:
28
29
  agents_service: Optional[AgentsService] = None,
29
30
  ):
30
31
  if agents_service is None:
31
- agentex_client = AsyncAgentex()
32
+ agentex_client = get_async_agentex_client()
32
33
  tracer = AsyncTracer(agentex_client)
33
34
  self._agents_service = AgentsService(agentex_client=agentex_client, tracer=tracer)
34
35
  else:
@@ -3,6 +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
7
  from agentex.lib.core.services.adk.events import EventsService
7
8
  from agentex.lib.core.temporal.activities.activity_helpers import ActivityHelpers
8
9
  from agentex.lib.core.temporal.activities.adk.events_activities import (
@@ -32,7 +33,7 @@ class EventsModule:
32
33
  events_service: EventsService | None = None,
33
34
  ):
34
35
  if events_service is None:
35
- agentex_client = AsyncAgentex()
36
+ agentex_client = get_async_agentex_client()
36
37
  tracer = AsyncTracer(agentex_client)
37
38
  self._events_service = EventsService(
38
39
  agentex_client=agentex_client, tracer=tracer
@@ -3,6 +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
7
  from agentex.lib.core.adapters.streams.adapter_redis import RedisStreamRepository
7
8
  from agentex.lib.core.services.adk.messages import MessagesService
8
9
  from agentex.lib.core.services.adk.streaming import StreamingService
@@ -37,7 +38,7 @@ class MessagesModule:
37
38
  messages_service: MessagesService | None = None,
38
39
  ):
39
40
  if messages_service is None:
40
- agentex_client = AsyncAgentex()
41
+ agentex_client = get_async_agentex_client()
41
42
  stream_repository = RedisStreamRepository()
42
43
  streaming_service = StreamingService(
43
44
  agentex_client=agentex_client,
@@ -5,6 +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
9
  from agentex.lib.core.services.adk.state import StateService
9
10
  from agentex.lib.core.temporal.activities.activity_helpers import ActivityHelpers
10
11
  from agentex.lib.core.temporal.activities.adk.state_activities import (
@@ -36,7 +37,7 @@ class StateModule:
36
37
  state_service: StateService | None = None,
37
38
  ):
38
39
  if state_service is None:
39
- agentex_client = AsyncAgentex()
40
+ agentex_client = get_async_agentex_client()
40
41
  tracer = AsyncTracer(agentex_client)
41
42
  self._state_service = StateService(
42
43
  agentex_client=agentex_client, tracer=tracer
@@ -1,6 +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
5
  from agentex.lib.core.adapters.streams.adapter_redis import RedisStreamRepository
5
6
  from agentex.lib.core.services.adk.streaming import (
6
7
  StreamingService,
@@ -34,7 +35,7 @@ class StreamingModule:
34
35
  """
35
36
  if streaming_service is None:
36
37
  stream_repository = RedisStreamRepository()
37
- agentex_client = AsyncAgentex()
38
+ agentex_client = get_async_agentex_client()
38
39
  self._streaming_service = StreamingService(
39
40
  agentex_client=agentex_client,
40
41
  stream_repository=stream_repository,
@@ -3,6 +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
7
  from agentex.lib.core.services.adk.tasks import TasksService
7
8
  from agentex.lib.core.temporal.activities.activity_helpers import ActivityHelpers
8
9
  from agentex.lib.core.temporal.activities.adk.tasks_activities import (
@@ -31,7 +32,7 @@ class TasksModule:
31
32
  tasks_service: TasksService | None = None,
32
33
  ):
33
34
  if tasks_service is None:
34
- agentex_client = AsyncAgentex()
35
+ agentex_client = get_async_agentex_client()
35
36
  tracer = AsyncTracer(agentex_client)
36
37
  self._tasks_service = TasksService(
37
38
  agentex_client=agentex_client, tracer=tracer
@@ -6,6 +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
10
  from agentex.lib.core.services.adk.tracing import TracingService
10
11
  from agentex.lib.core.temporal.activities.activity_helpers import ActivityHelpers
11
12
  from agentex.lib.core.temporal.activities.adk.tracing_activities import (
@@ -38,7 +39,7 @@ class TracingModule:
38
39
  tracing_activities (Optional[TracingActivities]): Optional pre-configured tracing activities. If None, will be auto-initialized.
39
40
  """
40
41
  if tracing_service is None:
41
- agentex_client = AsyncAgentex()
42
+ agentex_client = get_async_agentex_client()
42
43
  tracer = AsyncTracer(agentex_client)
43
44
  self._tracing_service = TracingService(tracer=tracer)
44
45
  else:
@@ -0,0 +1,43 @@
1
+ import threading
2
+ from typing import Dict, Optional, Any
3
+
4
+ from agentex import AsyncAgentex
5
+ from agentex.lib.environment_variables import EnvironmentVariables, refreshed_environment_variables
6
+
7
+ _client: Optional["AsyncAgentex"] = None
8
+ _cached_headers: Dict[str, str] = {}
9
+ _init_kwargs: Dict[str, Any] = {}
10
+ _lock = threading.RLock()
11
+
12
+
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 {}
18
+
19
+
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
+
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
@@ -141,13 +141,13 @@ def build(
141
141
  typer.echo("No registry provided, skipping image build")
142
142
  return
143
143
 
144
- platform_list = platforms.split(",") if platforms else []
144
+ platform_list = platforms.split(",") if platforms else ["linux/amd64"]
145
145
 
146
146
  try:
147
147
  image_url = build_agent(
148
148
  manifest_path=manifest,
149
- registry_url=registry, # Now guaranteed to be non-None
150
- repository_name=repository_name or "default-repo", # Provide default
149
+ registry_url=registry,
150
+ repository_name=repository_name,
151
151
  platforms=platform_list,
152
152
  push=push,
153
153
  secret=secret or "", # Provide default empty string
@@ -21,7 +21,7 @@ class DockerBuildError(Exception):
21
21
  def build_agent(
22
22
  manifest_path: str,
23
23
  registry_url: str,
24
- repository_name: str,
24
+ repository_name: str | None,
25
25
  platforms: list[str],
26
26
  push: bool = False,
27
27
  secret: str = None,
@@ -168,19 +168,13 @@ def cleanup_single_task(client: Agentex, agent_name: str, task_id: str) -> None:
168
168
  """
169
169
  try:
170
170
  # Use the agent RPC method to cancel the task
171
- try:
172
- client.agents.rpc_by_name(
173
- agent_name=agent_name,
174
- method="task/cancel",
175
- params={"task_id": task_id}
176
- )
177
- logger.debug(f"Successfully cancelled task {task_id} via agent '{agent_name}'")
178
- except Exception as e:
179
- # If RPC cancel fails, try direct task deletion as fallback
180
- logger.warning(f"RPC task/cancel failed for task {task_id}, trying direct deletion: {e}")
181
- client.tasks.delete(task_id=task_id)
182
- logger.debug(f"Successfully deleted task {task_id} directly")
183
-
171
+ client.agents.rpc_by_name(
172
+ agent_name=agent_name,
173
+ method="task/cancel",
174
+ params={"task_id": task_id}
175
+ )
176
+ logger.debug(f"Successfully cancelled task {task_id} via agent '{agent_name}'")
177
+
184
178
  except Exception as e:
185
- logger.warning(f"Failed to cleanup task {task_id}: {e}")
186
- raise
179
+ logger.warning(f"RPC task/cancel failed for task {task_id}: {e}")
180
+ raise
@@ -11,6 +11,7 @@ from rich.console import Console
11
11
  from agentex.lib.cli.utils.auth_utils import _encode_principal_context
12
12
  from agentex.lib.cli.utils.exceptions import DeploymentError, HelmError
13
13
  from agentex.lib.cli.utils.kubectl_utils import check_and_switch_cluster_context
14
+ from agentex.lib.cli.utils.path_utils import calculate_docker_acp_module, PathResolutionError
14
15
  from agentex.lib.environment_variables import EnvVarKeys
15
16
  from agentex.lib.sdk.config.agent_config import AgentConfig
16
17
  from agentex.lib.sdk.config.agent_manifest import AgentManifest
@@ -100,10 +101,24 @@ def convert_env_vars_dict_to_list(env_vars: dict[str, str]) -> list[dict[str, st
100
101
  return [{"name": key, "value": value} for key, value in env_vars.items()]
101
102
 
102
103
 
104
+ def add_acp_command_to_helm_values(helm_values: dict[str, Any], manifest: AgentManifest, manifest_path: str) -> None:
105
+ """Add dynamic ACP command to helm values based on manifest configuration"""
106
+ try:
107
+ docker_acp_module = calculate_docker_acp_module(manifest, manifest_path)
108
+ # Create the uvicorn command with the correct module path
109
+ helm_values["command"] = ["uvicorn", f"{docker_acp_module}:acp", "--host", "0.0.0.0", "--port", "8000"]
110
+ logger.info(f"Using dynamic ACP command: uvicorn {docker_acp_module}:acp")
111
+ except (PathResolutionError, Exception) as e:
112
+ # Fallback to default command structure
113
+ logger.warning(f"Could not calculate dynamic ACP module ({e}), using default: project.acp")
114
+ helm_values["command"] = ["uvicorn", "project.acp:acp", "--host", "0.0.0.0", "--port", "8000"]
115
+
116
+
103
117
  def merge_deployment_configs(
104
118
  manifest: AgentManifest,
105
119
  cluster_config: ClusterConfig | None,
106
120
  deploy_overrides: InputDeployOverrides,
121
+ manifest_path: str,
107
122
  ) -> dict[str, Any]:
108
123
  agent_config: AgentConfig = manifest.agent
109
124
 
@@ -176,9 +191,12 @@ def merge_deployment_configs(
176
191
  if TEMPORAL_WORKER_KEY in helm_values:
177
192
  helm_values[TEMPORAL_WORKER_KEY]["env"] = agent_config.env
178
193
 
179
- encoded_principal = _encode_principal_context(manifest)
180
- if encoded_principal:
181
- helm_values["env"][EnvVarKeys.AUTH_PRINCIPAL_B64] = encoded_principal
194
+ # Add auth principal env var if manifest principal is set
195
+ encoded_principal = _encode_principal_context(manifest)
196
+ 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
182
200
 
183
201
  if manifest.deployment and manifest.deployment.imagePullSecrets:
184
202
  pull_secrets = [
@@ -228,10 +246,16 @@ def merge_deployment_configs(
228
246
  # Convert the env vars to a list of dictionaries
229
247
  if "env" in helm_values:
230
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
231
251
  if TEMPORAL_WORKER_KEY in helm_values and "env" in helm_values[TEMPORAL_WORKER_KEY]:
232
252
  helm_values[TEMPORAL_WORKER_KEY]["env"] = convert_env_vars_dict_to_list(
233
253
  helm_values[TEMPORAL_WORKER_KEY]["env"]
234
254
  )
255
+
256
+ # Add dynamic ACP command based on manifest configuration
257
+ add_acp_command_to_helm_values(helm_values, manifest, manifest_path)
258
+
235
259
  print("Deploying with the following helm values: ", helm_values)
236
260
  return helm_values
237
261
 
@@ -287,7 +311,7 @@ def deploy_agent(
287
311
  add_helm_repo()
288
312
 
289
313
  # Merge configurations
290
- helm_values = merge_deployment_configs(manifest, override_config, deploy_overrides)
314
+ helm_values = merge_deployment_configs(manifest, override_config, deploy_overrides, manifest_path)
291
315
 
292
316
  # Create values file
293
317
  values_file = create_helm_values_file(helm_values)
@@ -11,6 +11,11 @@ from agentex.lib.cli.handlers.cleanup_handlers import (
11
11
  cleanup_agent_workflows,
12
12
  should_cleanup_on_restart
13
13
  )
14
+ from agentex.lib.cli.utils.path_utils import (
15
+ get_file_paths,
16
+ calculate_uvicorn_target_for_local,
17
+ )
18
+
14
19
  from agentex.lib.environment_variables import EnvVarKeys
15
20
  from agentex.lib.sdk.config.agent_manifest import AgentManifest
16
21
  from agentex.lib.utils.logging import make_logger
@@ -104,7 +109,10 @@ async def start_temporal_worker_with_reload(
104
109
  # PRE-RESTART CLEANUP - NEW!
105
110
  if current_process is not None:
106
111
  # Extract agent name from worker path for cleanup
107
- agent_name = worker_path.parent.parent.name
112
+
113
+ agent_name = env.get("AGENT_NAME")
114
+ if agent_name is None:
115
+ agent_name = worker_path.parent.parent.name
108
116
 
109
117
  # Perform cleanup if configured
110
118
  if should_cleanup_on_restart():
@@ -180,15 +188,17 @@ async def start_temporal_worker_with_reload(
180
188
 
181
189
 
182
190
  async def start_acp_server(
183
- acp_path: Path, port: int, env: dict[str, str]
191
+ acp_path: Path, port: int, env: dict[str, str], manifest_dir: Path
184
192
  ) -> asyncio.subprocess.Process:
185
193
  """Start the ACP server process"""
186
- # Use the actual file path instead of module path for better reload detection
194
+ # Use file path relative to manifest directory if possible
195
+ uvicorn_target = calculate_uvicorn_target_for_local(acp_path, manifest_dir)
196
+
187
197
  cmd = [
188
198
  sys.executable,
189
199
  "-m",
190
200
  "uvicorn",
191
- f"{acp_path.parent.name}.acp:acp",
201
+ f"{uvicorn_target}:acp",
192
202
  "--reload",
193
203
  "--reload-dir",
194
204
  str(acp_path.parent), # Watch the project directory specifically
@@ -201,7 +211,7 @@ async def start_acp_server(
201
211
  console.print(f"[blue]Starting ACP server from {acp_path} on port {port}...[/blue]")
202
212
  return await asyncio.create_subprocess_exec(
203
213
  *cmd,
204
- cwd=acp_path.parent.parent,
214
+ cwd=manifest_dir, # Always use manifest directory as CWD for consistency
205
215
  env=env,
206
216
  stdout=asyncio.subprocess.PIPE,
207
217
  stderr=asyncio.subprocess.STDOUT,
@@ -218,7 +228,7 @@ async def start_temporal_worker(
218
228
 
219
229
  return await asyncio.create_subprocess_exec(
220
230
  *cmd,
221
- cwd=worker_path.parent,
231
+ cwd=worker_path.parent, # Use worker directory as CWD for imports to work
222
232
  env=env,
223
233
  stdout=asyncio.subprocess.PIPE,
224
234
  stderr=asyncio.subprocess.STDOUT,
@@ -280,8 +290,9 @@ async def run_agent(manifest_path: str):
280
290
  )
281
291
 
282
292
  # Start ACP server
293
+ manifest_dir = Path(manifest_path).parent
283
294
  acp_process = await start_acp_server(
284
- file_paths["acp"], manifest.local_development.agent.port, agent_env
295
+ file_paths["acp"], manifest.local_development.agent.port, agent_env, manifest_dir
285
296
  )
286
297
  process_manager.add_process(acp_process)
287
298
 
@@ -291,7 +302,7 @@ async def run_agent(manifest_path: str):
291
302
  tasks = [acp_output_task]
292
303
 
293
304
  # Start temporal worker if needed
294
- if is_temporal_agent(manifest):
305
+ if is_temporal_agent(manifest) and file_paths["worker"]:
295
306
  worker_task = await start_temporal_worker_with_reload(file_paths["worker"], agent_env, process_manager)
296
307
  tasks.append(worker_task)
297
308
 
@@ -323,92 +334,7 @@ async def run_agent(manifest_path: str):
323
334
  await process_manager.cleanup_processes()
324
335
 
325
336
 
326
- def resolve_and_validate_path(base_path: Path, configured_path: str, file_type: str) -> Path:
327
- """Resolve and validate a configured path"""
328
- path_obj = Path(configured_path)
329
-
330
- if path_obj.is_absolute():
331
- # Absolute path - use as-is
332
- resolved_path = path_obj
333
- else:
334
- # Relative path - resolve relative to manifest directory
335
- resolved_path = (base_path / configured_path).resolve()
336
-
337
- # Validate the file exists
338
- if not resolved_path.exists():
339
- raise RunError(
340
- f"{file_type} file not found: {resolved_path}\n"
341
- f" Configured path: {configured_path}\n"
342
- f" Resolved from manifest: {base_path}"
343
- )
344
-
345
- # Validate it's actually a file
346
- if not resolved_path.is_file():
347
- raise RunError(f"{file_type} path is not a file: {resolved_path}")
348
-
349
- return resolved_path
350
-
351
-
352
- def validate_path_security(resolved_path: Path, manifest_dir: Path) -> None:
353
- """Basic security validation for resolved paths"""
354
- try:
355
- # Ensure the resolved path is accessible
356
- resolved_path.resolve()
357
-
358
- # Optional: Add warnings for paths that go too far up
359
- try:
360
- # Check if path goes more than 3 levels up from manifest
361
- relative_to_manifest = resolved_path.relative_to(manifest_dir.parent.parent.parent)
362
- if str(relative_to_manifest).startswith(".."):
363
- logger.warning(
364
- f"Path goes significantly outside project structure: {resolved_path}"
365
- )
366
- except ValueError:
367
- # Path is outside the tree - that's okay, just log it
368
- logger.info(f"Using path outside manifest directory tree: {resolved_path}")
369
-
370
- except Exception as e:
371
- raise RunError(f"Path resolution failed: {resolved_path} - {str(e)}") from e
372
-
373
337
 
374
- def get_file_paths(manifest: AgentManifest, manifest_path: str) -> dict[str, Path]:
375
- """Get resolved file paths from manifest configuration"""
376
- manifest_dir = Path(manifest_path).parent.resolve()
377
-
378
- # Use configured paths or fall back to defaults for backward compatibility
379
- if manifest.local_development and manifest.local_development.paths:
380
- paths_config = manifest.local_development.paths
381
-
382
- # Resolve ACP path
383
- acp_path = resolve_and_validate_path(manifest_dir, paths_config.acp, "ACP server")
384
- validate_path_security(acp_path, manifest_dir)
385
-
386
- # Resolve worker path if specified
387
- worker_path = None
388
- if paths_config.worker:
389
- worker_path = resolve_and_validate_path(
390
- manifest_dir, paths_config.worker, "Temporal worker"
391
- )
392
- validate_path_security(worker_path, manifest_dir)
393
- else:
394
- # Backward compatibility: use old hardcoded structure
395
- project_dir = manifest_dir / "project"
396
- acp_path = project_dir / "acp.py"
397
- worker_path = project_dir / "run_worker.py" if is_temporal_agent(manifest) else None
398
-
399
- # Validate backward compatibility paths
400
- if not acp_path.exists():
401
- raise RunError(f"ACP file not found: {acp_path}")
402
-
403
- if worker_path and not worker_path.exists():
404
- raise RunError(f"Worker file not found: {worker_path}")
405
-
406
- return {
407
- "acp": acp_path,
408
- "worker": worker_path,
409
- "acp_dir": acp_path.parent,
410
- "worker_dir": worker_path.parent if worker_path else None,
411
- }
412
338
 
413
339
 
414
340
  def create_agent_environment(manifest: AgentManifest) -> dict[str, str]:
@@ -1,75 +1,26 @@
1
- import json
2
- from agentex.lib import adk
1
+ from typing import AsyncGenerator, Union
3
2
  from agentex.lib.sdk.fastacp.fastacp import FastACP
4
- from agentex.lib.types.fastacp import AgenticACPConfig
5
- from agentex.lib.types.acp import CancelTaskParams, CreateTaskParams, SendEventParams
3
+ from agentex.lib.types.acp import SendMessageParams
6
4
 
5
+ from agentex.lib.types.task_message_updates import TaskMessageUpdate
6
+ from agentex.types.task_message_content import TaskMessageContent
7
7
  from agentex.types.text_content import TextContent
8
8
  from agentex.lib.utils.logging import make_logger
9
9
 
10
10
  logger = make_logger(__name__)
11
11
 
12
12
 
13
- # Create an ACP server with base configuration
14
- # This sets up the core server that will handle task creation, events, and cancellation
13
+ # Create an ACP server
15
14
  acp = FastACP.create(
16
- acp_type="agentic",
17
- config=AgenticACPConfig(
18
- type="base",
19
- ),
15
+ acp_type="sync",
20
16
  )
21
17
 
22
- @acp.on_task_create
23
- async def handle_task_create(params: CreateTaskParams):
24
- # This handler is called first whenever a new task is created.
25
- # It's a good place to initialize any state or resources needed for the task.
26
-
27
- #########################################################
28
- # 1. (👋) Do task initialization here.
29
- #########################################################
30
-
31
- # Acknowledge that the task has been created.
32
- await adk.messages.create(
33
- task_id=params.task.id,
34
- content=TextContent(
35
- author="agent",
36
- content=f"Hello! I've received your task. Normally you can do some state initialization here, or just pass and do nothing until you get your first event. For now I'm just acknowledging that I've received a task with the following params:\n\n{json.dumps(params.params, indent=2)}.\n\nYou should only see this message once, when the task is created. All subsequent events will be handled by the `on_task_event_send` handler.",
37
- ),
38
- )
39
-
40
- @acp.on_task_event_send
41
- async def handle_event_send(params: SendEventParams):
42
- # This handler is called whenever a new event (like a message) is sent to the task
43
-
44
- #########################################################
45
- # 2. (👋) Echo back the client's message to show it in the UI.
46
- #########################################################
47
-
48
- # This is not done by default so the agent developer has full control over what is shown to the user.
49
- if params.event.content:
50
- await adk.messages.create(task_id=params.task.id, content=params.event.content)
51
-
52
- #########################################################
53
- # 3. (👋) Send a simple response message.
54
- #########################################################
55
-
56
- # In future tutorials, this is where we'll add more sophisticated response logic.
57
- await adk.messages.create(
58
- task_id=params.task.id,
59
- content=TextContent(
60
- author="agent",
61
- content=f"Hello! I've received your message. I can't respond right now, but in future tutorials we'll see how you can get me to intelligently respond to your message.",
62
- ),
63
- )
64
-
65
- @acp.on_task_cancel
66
- async def handle_task_cancel(params: CancelTaskParams):
67
- # This handler is called when a task is cancelled.
68
- # It's useful for cleaning up any resources or state associated with the task.
69
-
70
- #########################################################
71
- # 4. (👋) Do task cleanup here.
72
- #########################################################
73
-
74
- # This is mostly for durable workflows that are cancellable like Temporal, but we will leave it here for demonstration purposes.
75
- logger.info(f"Hello! I've received task cancel for task {params.task.id}: {params.task}. This isn't necessary for this example, but it's good to know that it's available.")
18
+ @acp.on_message_send
19
+ async def handle_message_send(
20
+ params: SendMessageParams
21
+ ) -> TaskMessageContent | list[TaskMessageContent] | AsyncGenerator[TaskMessageUpdate, None]:
22
+ """Default message handler with streaming support"""
23
+ return TextContent(
24
+ author="agent",
25
+ content=f"Hello! I've received your message. Here's a generic response, but in future tutorials we'll see how you can get me to intelligently respond to your message. This is what I heard you say: {params.content.content}",
26
+ )