agentex-sdk 0.1.0a6__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/__init__.py +103 -0
- agentex/_base_client.py +1992 -0
- agentex/_client.py +506 -0
- agentex/_compat.py +219 -0
- agentex/_constants.py +14 -0
- agentex/_exceptions.py +108 -0
- agentex/_files.py +123 -0
- agentex/_models.py +829 -0
- agentex/_qs.py +150 -0
- agentex/_resource.py +43 -0
- agentex/_response.py +830 -0
- agentex/_streaming.py +333 -0
- agentex/_types.py +219 -0
- agentex/_utils/__init__.py +57 -0
- agentex/_utils/_logs.py +25 -0
- agentex/_utils/_proxy.py +65 -0
- agentex/_utils/_reflection.py +42 -0
- agentex/_utils/_resources_proxy.py +24 -0
- agentex/_utils/_streams.py +12 -0
- agentex/_utils/_sync.py +86 -0
- agentex/_utils/_transform.py +447 -0
- agentex/_utils/_typing.py +151 -0
- agentex/_utils/_utils.py +422 -0
- agentex/_version.py +4 -0
- agentex/lib/.keep +4 -0
- agentex/lib/__init__.py +0 -0
- agentex/lib/adk/__init__.py +41 -0
- agentex/lib/adk/_modules/__init__.py +0 -0
- agentex/lib/adk/_modules/acp.py +247 -0
- agentex/lib/adk/_modules/agent_task_tracker.py +176 -0
- agentex/lib/adk/_modules/agents.py +77 -0
- agentex/lib/adk/_modules/events.py +141 -0
- agentex/lib/adk/_modules/messages.py +285 -0
- agentex/lib/adk/_modules/state.py +291 -0
- agentex/lib/adk/_modules/streaming.py +75 -0
- agentex/lib/adk/_modules/tasks.py +124 -0
- agentex/lib/adk/_modules/tracing.py +194 -0
- agentex/lib/adk/providers/__init__.py +9 -0
- agentex/lib/adk/providers/_modules/__init__.py +0 -0
- agentex/lib/adk/providers/_modules/litellm.py +232 -0
- agentex/lib/adk/providers/_modules/openai.py +416 -0
- agentex/lib/adk/providers/_modules/sgp.py +85 -0
- agentex/lib/adk/utils/__init__.py +5 -0
- agentex/lib/adk/utils/_modules/__init__.py +0 -0
- agentex/lib/adk/utils/_modules/templating.py +94 -0
- agentex/lib/cli/__init__.py +0 -0
- agentex/lib/cli/commands/__init__.py +0 -0
- agentex/lib/cli/commands/agents.py +328 -0
- agentex/lib/cli/commands/init.py +227 -0
- agentex/lib/cli/commands/main.py +33 -0
- agentex/lib/cli/commands/secrets.py +169 -0
- agentex/lib/cli/commands/tasks.py +118 -0
- agentex/lib/cli/commands/uv.py +133 -0
- agentex/lib/cli/handlers/__init__.py +0 -0
- agentex/lib/cli/handlers/agent_handlers.py +160 -0
- agentex/lib/cli/handlers/cleanup_handlers.py +186 -0
- agentex/lib/cli/handlers/deploy_handlers.py +351 -0
- agentex/lib/cli/handlers/run_handlers.py +452 -0
- agentex/lib/cli/handlers/secret_handlers.py +670 -0
- agentex/lib/cli/templates/default/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/default/Dockerfile-uv.j2 +42 -0
- agentex/lib/cli/templates/default/Dockerfile.j2 +42 -0
- agentex/lib/cli/templates/default/README.md.j2 +193 -0
- agentex/lib/cli/templates/default/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/default/manifest.yaml.j2 +116 -0
- agentex/lib/cli/templates/default/project/acp.py.j2 +29 -0
- agentex/lib/cli/templates/default/pyproject.toml.j2 +33 -0
- agentex/lib/cli/templates/default/requirements.txt.j2 +5 -0
- agentex/lib/cli/templates/deploy/Screenshot 2025-03-19 at 10.36.57/342/200/257AM.png +0 -0
- agentex/lib/cli/templates/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/sync/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/sync/Dockerfile-uv.j2 +42 -0
- agentex/lib/cli/templates/sync/Dockerfile.j2 +42 -0
- agentex/lib/cli/templates/sync/README.md.j2 +293 -0
- agentex/lib/cli/templates/sync/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/sync/manifest.yaml.j2 +116 -0
- agentex/lib/cli/templates/sync/project/acp.py.j2 +26 -0
- agentex/lib/cli/templates/sync/pyproject.toml.j2 +33 -0
- agentex/lib/cli/templates/sync/requirements.txt.j2 +5 -0
- agentex/lib/cli/templates/temporal/.dockerignore.j2 +43 -0
- agentex/lib/cli/templates/temporal/Dockerfile-uv.j2 +48 -0
- agentex/lib/cli/templates/temporal/Dockerfile.j2 +48 -0
- agentex/lib/cli/templates/temporal/README.md.j2 +316 -0
- agentex/lib/cli/templates/temporal/deploy/example.yaml.j2 +55 -0
- agentex/lib/cli/templates/temporal/manifest.yaml.j2 +137 -0
- agentex/lib/cli/templates/temporal/project/acp.py.j2 +30 -0
- agentex/lib/cli/templates/temporal/project/run_worker.py.j2 +33 -0
- agentex/lib/cli/templates/temporal/project/workflow.py.j2 +66 -0
- agentex/lib/cli/templates/temporal/pyproject.toml.j2 +34 -0
- agentex/lib/cli/templates/temporal/requirements.txt.j2 +5 -0
- agentex/lib/cli/utils/cli_utils.py +14 -0
- agentex/lib/cli/utils/credential_utils.py +103 -0
- agentex/lib/cli/utils/exceptions.py +6 -0
- agentex/lib/cli/utils/kubectl_utils.py +135 -0
- agentex/lib/cli/utils/kubernetes_secrets_utils.py +185 -0
- agentex/lib/core/__init__.py +0 -0
- agentex/lib/core/adapters/__init__.py +0 -0
- agentex/lib/core/adapters/llm/__init__.py +1 -0
- agentex/lib/core/adapters/llm/adapter_litellm.py +46 -0
- agentex/lib/core/adapters/llm/adapter_sgp.py +55 -0
- agentex/lib/core/adapters/llm/port.py +24 -0
- agentex/lib/core/adapters/streams/adapter_redis.py +128 -0
- agentex/lib/core/adapters/streams/port.py +50 -0
- agentex/lib/core/clients/__init__.py +1 -0
- agentex/lib/core/clients/temporal/__init__.py +0 -0
- agentex/lib/core/clients/temporal/temporal_client.py +181 -0
- agentex/lib/core/clients/temporal/types.py +47 -0
- agentex/lib/core/clients/temporal/utils.py +56 -0
- agentex/lib/core/services/__init__.py +0 -0
- agentex/lib/core/services/adk/__init__.py +0 -0
- agentex/lib/core/services/adk/acp/__init__.py +0 -0
- agentex/lib/core/services/adk/acp/acp.py +210 -0
- agentex/lib/core/services/adk/agent_task_tracker.py +85 -0
- agentex/lib/core/services/adk/agents.py +43 -0
- agentex/lib/core/services/adk/events.py +61 -0
- agentex/lib/core/services/adk/messages.py +164 -0
- agentex/lib/core/services/adk/providers/__init__.py +0 -0
- agentex/lib/core/services/adk/providers/litellm.py +256 -0
- agentex/lib/core/services/adk/providers/openai.py +723 -0
- agentex/lib/core/services/adk/providers/sgp.py +99 -0
- agentex/lib/core/services/adk/state.py +120 -0
- agentex/lib/core/services/adk/streaming.py +262 -0
- agentex/lib/core/services/adk/tasks.py +69 -0
- agentex/lib/core/services/adk/tracing.py +36 -0
- agentex/lib/core/services/adk/utils/__init__.py +0 -0
- agentex/lib/core/services/adk/utils/templating.py +58 -0
- agentex/lib/core/temporal/__init__.py +0 -0
- agentex/lib/core/temporal/activities/__init__.py +207 -0
- agentex/lib/core/temporal/activities/activity_helpers.py +37 -0
- agentex/lib/core/temporal/activities/adk/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/acp/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/acp/acp_activities.py +86 -0
- agentex/lib/core/temporal/activities/adk/agent_task_tracker_activities.py +76 -0
- agentex/lib/core/temporal/activities/adk/agents_activities.py +35 -0
- agentex/lib/core/temporal/activities/adk/events_activities.py +50 -0
- agentex/lib/core/temporal/activities/adk/messages_activities.py +94 -0
- agentex/lib/core/temporal/activities/adk/providers/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/providers/litellm_activities.py +71 -0
- agentex/lib/core/temporal/activities/adk/providers/openai_activities.py +210 -0
- agentex/lib/core/temporal/activities/adk/providers/sgp_activities.py +42 -0
- agentex/lib/core/temporal/activities/adk/state_activities.py +85 -0
- agentex/lib/core/temporal/activities/adk/streaming_activities.py +33 -0
- agentex/lib/core/temporal/activities/adk/tasks_activities.py +48 -0
- agentex/lib/core/temporal/activities/adk/tracing_activities.py +55 -0
- agentex/lib/core/temporal/activities/adk/utils/__init__.py +0 -0
- agentex/lib/core/temporal/activities/adk/utils/templating_activities.py +41 -0
- agentex/lib/core/temporal/services/__init__.py +0 -0
- agentex/lib/core/temporal/services/temporal_task_service.py +69 -0
- agentex/lib/core/temporal/types/__init__.py +0 -0
- agentex/lib/core/temporal/types/workflow.py +5 -0
- agentex/lib/core/temporal/workers/__init__.py +0 -0
- agentex/lib/core/temporal/workers/worker.py +162 -0
- agentex/lib/core/temporal/workflows/workflow.py +26 -0
- agentex/lib/core/tracing/__init__.py +5 -0
- agentex/lib/core/tracing/processors/agentex_tracing_processor.py +117 -0
- agentex/lib/core/tracing/processors/sgp_tracing_processor.py +119 -0
- agentex/lib/core/tracing/processors/tracing_processor_interface.py +40 -0
- agentex/lib/core/tracing/trace.py +311 -0
- agentex/lib/core/tracing/tracer.py +70 -0
- agentex/lib/core/tracing/tracing_processor_manager.py +62 -0
- agentex/lib/environment_variables.py +87 -0
- agentex/lib/py.typed +0 -0
- agentex/lib/sdk/__init__.py +0 -0
- agentex/lib/sdk/config/__init__.py +0 -0
- agentex/lib/sdk/config/agent_config.py +61 -0
- agentex/lib/sdk/config/agent_manifest.py +219 -0
- agentex/lib/sdk/config/build_config.py +35 -0
- agentex/lib/sdk/config/deployment_config.py +117 -0
- agentex/lib/sdk/config/local_development_config.py +56 -0
- agentex/lib/sdk/config/project_config.py +103 -0
- agentex/lib/sdk/fastacp/__init__.py +3 -0
- agentex/lib/sdk/fastacp/base/base_acp_server.py +406 -0
- agentex/lib/sdk/fastacp/fastacp.py +74 -0
- agentex/lib/sdk/fastacp/impl/agentic_base_acp.py +72 -0
- agentex/lib/sdk/fastacp/impl/sync_acp.py +109 -0
- agentex/lib/sdk/fastacp/impl/temporal_acp.py +97 -0
- agentex/lib/sdk/fastacp/tests/README.md +297 -0
- agentex/lib/sdk/fastacp/tests/conftest.py +307 -0
- agentex/lib/sdk/fastacp/tests/pytest.ini +10 -0
- agentex/lib/sdk/fastacp/tests/run_tests.py +227 -0
- agentex/lib/sdk/fastacp/tests/test_base_acp_server.py +450 -0
- agentex/lib/sdk/fastacp/tests/test_fastacp_factory.py +344 -0
- agentex/lib/sdk/fastacp/tests/test_integration.py +477 -0
- agentex/lib/sdk/state_machine/__init__.py +6 -0
- agentex/lib/sdk/state_machine/noop_workflow.py +21 -0
- agentex/lib/sdk/state_machine/state.py +10 -0
- agentex/lib/sdk/state_machine/state_machine.py +189 -0
- agentex/lib/sdk/state_machine/state_workflow.py +16 -0
- agentex/lib/sdk/utils/__init__.py +0 -0
- agentex/lib/sdk/utils/messages.py +223 -0
- agentex/lib/types/__init__.py +0 -0
- agentex/lib/types/acp.py +94 -0
- agentex/lib/types/agent_configs.py +79 -0
- agentex/lib/types/agent_results.py +29 -0
- agentex/lib/types/credentials.py +34 -0
- agentex/lib/types/fastacp.py +61 -0
- agentex/lib/types/files.py +13 -0
- agentex/lib/types/json_rpc.py +49 -0
- agentex/lib/types/llm_messages.py +354 -0
- agentex/lib/types/task_message_updates.py +171 -0
- agentex/lib/types/tracing.py +34 -0
- agentex/lib/utils/__init__.py +0 -0
- agentex/lib/utils/completions.py +131 -0
- agentex/lib/utils/console.py +14 -0
- agentex/lib/utils/io.py +29 -0
- agentex/lib/utils/iterables.py +14 -0
- agentex/lib/utils/json_schema.py +23 -0
- agentex/lib/utils/logging.py +31 -0
- agentex/lib/utils/mcp.py +17 -0
- agentex/lib/utils/model_utils.py +46 -0
- agentex/lib/utils/parsing.py +15 -0
- agentex/lib/utils/regex.py +6 -0
- agentex/lib/utils/temporal.py +13 -0
- agentex/py.typed +0 -0
- agentex/resources/__init__.py +103 -0
- agentex/resources/agents.py +707 -0
- agentex/resources/events.py +294 -0
- agentex/resources/messages/__init__.py +33 -0
- agentex/resources/messages/batch.py +271 -0
- agentex/resources/messages/messages.py +492 -0
- agentex/resources/spans.py +557 -0
- agentex/resources/states.py +544 -0
- agentex/resources/tasks.py +615 -0
- agentex/resources/tracker.py +384 -0
- agentex/types/__init__.py +56 -0
- agentex/types/acp_type.py +7 -0
- agentex/types/agent.py +29 -0
- agentex/types/agent_list_params.py +13 -0
- agentex/types/agent_list_response.py +10 -0
- agentex/types/agent_rpc_by_name_params.py +21 -0
- agentex/types/agent_rpc_params.py +51 -0
- agentex/types/agent_rpc_params1.py +21 -0
- agentex/types/agent_rpc_response.py +20 -0
- agentex/types/agent_rpc_result.py +90 -0
- agentex/types/agent_task_tracker.py +34 -0
- agentex/types/data_content.py +30 -0
- agentex/types/data_content_param.py +31 -0
- agentex/types/data_delta.py +14 -0
- agentex/types/event.py +29 -0
- agentex/types/event_list_params.py +22 -0
- agentex/types/event_list_response.py +10 -0
- agentex/types/message_author.py +7 -0
- agentex/types/message_create_params.py +18 -0
- agentex/types/message_list_params.py +14 -0
- agentex/types/message_list_response.py +10 -0
- agentex/types/message_style.py +7 -0
- agentex/types/message_update_params.py +18 -0
- agentex/types/messages/__init__.py +8 -0
- agentex/types/messages/batch_create_params.py +16 -0
- agentex/types/messages/batch_create_response.py +10 -0
- agentex/types/messages/batch_update_params.py +16 -0
- agentex/types/messages/batch_update_response.py +10 -0
- agentex/types/shared/__init__.py +3 -0
- agentex/types/shared/task_message_update.py +83 -0
- agentex/types/span.py +36 -0
- agentex/types/span_create_params.py +40 -0
- agentex/types/span_list_params.py +12 -0
- agentex/types/span_list_response.py +10 -0
- agentex/types/span_update_params.py +37 -0
- agentex/types/state.py +25 -0
- agentex/types/state_create_params.py +16 -0
- agentex/types/state_list_params.py +16 -0
- agentex/types/state_list_response.py +10 -0
- agentex/types/state_update_params.py +16 -0
- agentex/types/task.py +23 -0
- agentex/types/task_delete_by_name_response.py +8 -0
- agentex/types/task_delete_response.py +8 -0
- agentex/types/task_list_response.py +10 -0
- agentex/types/task_message.py +33 -0
- agentex/types/task_message_content.py +16 -0
- agentex/types/task_message_content_param.py +17 -0
- agentex/types/task_message_delta.py +16 -0
- agentex/types/text_content.py +53 -0
- agentex/types/text_content_param.py +54 -0
- agentex/types/text_delta.py +14 -0
- agentex/types/tool_request_content.py +36 -0
- agentex/types/tool_request_content_param.py +37 -0
- agentex/types/tool_request_delta.py +18 -0
- agentex/types/tool_response_content.py +36 -0
- agentex/types/tool_response_content_param.py +36 -0
- agentex/types/tool_response_delta.py +18 -0
- agentex/types/tracker_list_params.py +16 -0
- agentex/types/tracker_list_response.py +10 -0
- agentex/types/tracker_update_params.py +19 -0
- agentex_sdk-0.1.0a6.dist-info/METADATA +426 -0
- agentex_sdk-0.1.0a6.dist-info/RECORD +289 -0
- agentex_sdk-0.1.0a6.dist-info/WHEEL +4 -0
- agentex_sdk-0.1.0a6.dist-info/entry_points.txt +2 -0
- agentex_sdk-0.1.0a6.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,452 @@
|
|
1
|
+
import asyncio
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
from rich.console import Console
|
7
|
+
from rich.panel import Panel
|
8
|
+
|
9
|
+
from agentex.lib.cli.handlers.cleanup_handlers import (
|
10
|
+
cleanup_agent_workflows,
|
11
|
+
should_cleanup_on_restart
|
12
|
+
)
|
13
|
+
from agentex.lib.sdk.config.agent_manifest import AgentManifest
|
14
|
+
from agentex.lib.utils.logging import make_logger
|
15
|
+
|
16
|
+
logger = make_logger(__name__)
|
17
|
+
console = Console()
|
18
|
+
|
19
|
+
|
20
|
+
class RunError(Exception):
|
21
|
+
"""An error occurred during agent run"""
|
22
|
+
|
23
|
+
|
24
|
+
class ProcessManager:
|
25
|
+
"""Manages multiple subprocesses with proper cleanup"""
|
26
|
+
|
27
|
+
def __init__(self):
|
28
|
+
self.processes: list[asyncio.subprocess.Process] = []
|
29
|
+
self.shutdown_event = asyncio.Event()
|
30
|
+
|
31
|
+
def add_process(self, process: asyncio.subprocess.Process):
|
32
|
+
"""Add a process to be managed"""
|
33
|
+
self.processes.append(process)
|
34
|
+
|
35
|
+
async def wait_for_shutdown(self):
|
36
|
+
"""Wait for shutdown signal"""
|
37
|
+
await self.shutdown_event.wait()
|
38
|
+
|
39
|
+
def shutdown(self):
|
40
|
+
"""Signal shutdown and terminate all processes"""
|
41
|
+
self.shutdown_event.set()
|
42
|
+
|
43
|
+
async def cleanup_processes(self):
|
44
|
+
"""Clean up all processes"""
|
45
|
+
if not self.processes:
|
46
|
+
return
|
47
|
+
|
48
|
+
console.print("\n[yellow]Shutting down processes...[/yellow]")
|
49
|
+
|
50
|
+
# Send SIGTERM to all processes
|
51
|
+
for process in self.processes:
|
52
|
+
if process.returncode is None: # Process is still running
|
53
|
+
try:
|
54
|
+
process.terminate()
|
55
|
+
except ProcessLookupError:
|
56
|
+
pass # Process already terminated
|
57
|
+
|
58
|
+
# Wait for graceful shutdown with shorter timeout
|
59
|
+
try:
|
60
|
+
await asyncio.wait_for(
|
61
|
+
asyncio.gather(*[p.wait() for p in self.processes], return_exceptions=True),
|
62
|
+
timeout=2.0, # Reduced from 5.0 seconds
|
63
|
+
)
|
64
|
+
except TimeoutError:
|
65
|
+
# Force kill if not terminated gracefully
|
66
|
+
console.print("[yellow]Force killing unresponsive processes...[/yellow]")
|
67
|
+
for process in self.processes:
|
68
|
+
if process.returncode is None:
|
69
|
+
try:
|
70
|
+
process.kill()
|
71
|
+
await asyncio.wait_for(process.wait(), timeout=1.0)
|
72
|
+
except (ProcessLookupError, TimeoutError):
|
73
|
+
pass # Process already dead or kill failed
|
74
|
+
|
75
|
+
console.print("[green]All processes stopped[/green]")
|
76
|
+
|
77
|
+
|
78
|
+
async def start_temporal_worker_with_reload(
|
79
|
+
worker_path: Path, env: dict[str, str], process_manager: ProcessManager
|
80
|
+
) -> asyncio.Task[None]:
|
81
|
+
"""Start temporal worker with auto-reload using watchfiles"""
|
82
|
+
|
83
|
+
try:
|
84
|
+
from watchfiles import awatch
|
85
|
+
except ImportError:
|
86
|
+
console.print("[yellow]watchfiles not installed, falling back to basic worker start[/yellow]")
|
87
|
+
console.print("[dim]Install with: pip install watchfiles[/dim]")
|
88
|
+
# Fallback to regular worker without reload
|
89
|
+
worker_process = await start_temporal_worker(worker_path, env)
|
90
|
+
process_manager.add_process(worker_process)
|
91
|
+
return asyncio.create_task(stream_process_output(worker_process, "WORKER"))
|
92
|
+
|
93
|
+
async def worker_runner() -> None:
|
94
|
+
current_process: asyncio.subprocess.Process | None = None
|
95
|
+
output_task: asyncio.Task[None] | None = None
|
96
|
+
|
97
|
+
console.print(f"[blue]Starting Temporal worker with auto-reload from {worker_path}...[/blue]")
|
98
|
+
|
99
|
+
async def start_worker() -> asyncio.subprocess.Process:
|
100
|
+
nonlocal current_process, output_task
|
101
|
+
|
102
|
+
# PRE-RESTART CLEANUP - NEW!
|
103
|
+
if current_process is not None:
|
104
|
+
# Extract agent name from worker path for cleanup
|
105
|
+
agent_name = worker_path.parent.parent.name
|
106
|
+
|
107
|
+
# Perform cleanup if configured
|
108
|
+
if should_cleanup_on_restart():
|
109
|
+
console.print("[yellow]Cleaning up workflows before worker restart...[/yellow]")
|
110
|
+
try:
|
111
|
+
cleanup_agent_workflows(agent_name)
|
112
|
+
except Exception as e:
|
113
|
+
logger.warning(f"Cleanup failed: {e}")
|
114
|
+
console.print(f"[yellow]⚠ Cleanup failed: {str(e)}[/yellow]")
|
115
|
+
|
116
|
+
# Clean up previous process
|
117
|
+
if current_process and current_process.returncode is None:
|
118
|
+
current_process.terminate()
|
119
|
+
try:
|
120
|
+
await asyncio.wait_for(current_process.wait(), timeout=2.0)
|
121
|
+
except asyncio.TimeoutError:
|
122
|
+
current_process.kill()
|
123
|
+
await current_process.wait()
|
124
|
+
|
125
|
+
# Cancel previous output task
|
126
|
+
if output_task:
|
127
|
+
output_task.cancel()
|
128
|
+
try:
|
129
|
+
await output_task
|
130
|
+
except asyncio.CancelledError:
|
131
|
+
pass
|
132
|
+
|
133
|
+
current_process = await start_temporal_worker(worker_path, env)
|
134
|
+
process_manager.add_process(current_process)
|
135
|
+
console.print("[green]Temporal worker started[/green]")
|
136
|
+
return current_process
|
137
|
+
|
138
|
+
try:
|
139
|
+
# Start initial worker
|
140
|
+
await start_worker()
|
141
|
+
if current_process:
|
142
|
+
output_task = asyncio.create_task(stream_process_output(current_process, "WORKER"))
|
143
|
+
|
144
|
+
# Watch for file changes
|
145
|
+
async for changes in awatch(worker_path.parent):
|
146
|
+
# Filter for Python files
|
147
|
+
py_changes = [(change, path) for change, path in changes if str(path).endswith('.py')]
|
148
|
+
|
149
|
+
if py_changes:
|
150
|
+
changed_files = [str(Path(path).relative_to(worker_path.parent)) for _, path in py_changes]
|
151
|
+
console.print(f"[yellow]File changes detected: {changed_files}[/yellow]")
|
152
|
+
console.print("[yellow]Restarting Temporal worker...[/yellow]")
|
153
|
+
|
154
|
+
# Restart worker (with cleanup handled in start_worker)
|
155
|
+
await start_worker()
|
156
|
+
if current_process:
|
157
|
+
output_task = asyncio.create_task(stream_process_output(current_process, "WORKER"))
|
158
|
+
|
159
|
+
except asyncio.CancelledError:
|
160
|
+
# Clean shutdown
|
161
|
+
if output_task:
|
162
|
+
output_task.cancel()
|
163
|
+
try:
|
164
|
+
await output_task
|
165
|
+
except asyncio.CancelledError:
|
166
|
+
pass
|
167
|
+
|
168
|
+
if current_process and current_process.returncode is None:
|
169
|
+
current_process.terminate()
|
170
|
+
try:
|
171
|
+
await asyncio.wait_for(current_process.wait(), timeout=2.0)
|
172
|
+
except asyncio.TimeoutError:
|
173
|
+
current_process.kill()
|
174
|
+
await current_process.wait()
|
175
|
+
raise
|
176
|
+
|
177
|
+
return asyncio.create_task(worker_runner())
|
178
|
+
|
179
|
+
|
180
|
+
async def start_acp_server(
|
181
|
+
acp_path: Path, port: int, env: dict[str, str]
|
182
|
+
) -> asyncio.subprocess.Process:
|
183
|
+
"""Start the ACP server process"""
|
184
|
+
# Use the actual file path instead of module path for better reload detection
|
185
|
+
cmd = [
|
186
|
+
sys.executable,
|
187
|
+
"-m",
|
188
|
+
"uvicorn",
|
189
|
+
f"{acp_path.parent.name}.acp:acp",
|
190
|
+
"--reload",
|
191
|
+
"--reload-dir",
|
192
|
+
str(acp_path.parent), # Watch the project directory specifically
|
193
|
+
"--port",
|
194
|
+
str(port),
|
195
|
+
"--host",
|
196
|
+
"0.0.0.0",
|
197
|
+
]
|
198
|
+
|
199
|
+
console.print(f"[blue]Starting ACP server from {acp_path} on port {port}...[/blue]")
|
200
|
+
return await asyncio.create_subprocess_exec(
|
201
|
+
*cmd,
|
202
|
+
cwd=acp_path.parent.parent,
|
203
|
+
env=env,
|
204
|
+
stdout=asyncio.subprocess.PIPE,
|
205
|
+
stderr=asyncio.subprocess.STDOUT,
|
206
|
+
)
|
207
|
+
|
208
|
+
|
209
|
+
async def start_temporal_worker(
|
210
|
+
worker_path: Path, env: dict[str, str]
|
211
|
+
) -> asyncio.subprocess.Process:
|
212
|
+
"""Start the temporal worker process"""
|
213
|
+
cmd = [sys.executable, "-m", "run_worker"]
|
214
|
+
|
215
|
+
console.print(f"[blue]Starting Temporal worker from {worker_path}...[/blue]")
|
216
|
+
|
217
|
+
return await asyncio.create_subprocess_exec(
|
218
|
+
*cmd,
|
219
|
+
cwd=worker_path.parent,
|
220
|
+
env=env,
|
221
|
+
stdout=asyncio.subprocess.PIPE,
|
222
|
+
stderr=asyncio.subprocess.STDOUT,
|
223
|
+
)
|
224
|
+
|
225
|
+
|
226
|
+
async def stream_process_output(process: asyncio.subprocess.Process, prefix: str):
|
227
|
+
"""Stream process output with prefix"""
|
228
|
+
try:
|
229
|
+
while True:
|
230
|
+
line = await process.stdout.readline()
|
231
|
+
if not line:
|
232
|
+
break
|
233
|
+
decoded_line = line.decode("utf-8").rstrip()
|
234
|
+
if decoded_line: # Only print non-empty lines
|
235
|
+
console.print(f"[dim]{prefix}:[/dim] {decoded_line}")
|
236
|
+
except Exception as e:
|
237
|
+
logger.debug(f"Output streaming ended for {prefix}: {e}")
|
238
|
+
|
239
|
+
|
240
|
+
async def run_agent(manifest_path: str):
|
241
|
+
"""Run an agent locally from the given manifest"""
|
242
|
+
|
243
|
+
# Validate manifest exists
|
244
|
+
manifest_file = Path(manifest_path)
|
245
|
+
|
246
|
+
if not manifest_file.exists():
|
247
|
+
raise RunError(f"Manifest file not found: {manifest_path}")
|
248
|
+
|
249
|
+
# Parse manifest
|
250
|
+
try:
|
251
|
+
manifest = AgentManifest.from_yaml(file_path=manifest_path)
|
252
|
+
except Exception as e:
|
253
|
+
raise RunError(f"Failed to parse manifest: {str(e)}") from e
|
254
|
+
|
255
|
+
# Get and validate file paths
|
256
|
+
try:
|
257
|
+
file_paths = get_file_paths(manifest, manifest_path)
|
258
|
+
except Exception as e:
|
259
|
+
raise RunError(str(e)) from e
|
260
|
+
|
261
|
+
# Check if temporal agent and validate worker file
|
262
|
+
if is_temporal_agent(manifest):
|
263
|
+
if not file_paths["worker"]:
|
264
|
+
raise RunError("Temporal agent requires a worker file path to be configured")
|
265
|
+
|
266
|
+
# Create environment for subprocesses
|
267
|
+
agent_env = create_agent_environment(manifest)
|
268
|
+
|
269
|
+
# Setup process manager
|
270
|
+
process_manager = ProcessManager()
|
271
|
+
|
272
|
+
try:
|
273
|
+
console.print(
|
274
|
+
Panel.fit(
|
275
|
+
f"🚀 [bold blue]Running Agent: {manifest.agent.name}[/bold blue]",
|
276
|
+
border_style="blue",
|
277
|
+
)
|
278
|
+
)
|
279
|
+
|
280
|
+
# Start ACP server
|
281
|
+
acp_process = await start_acp_server(
|
282
|
+
file_paths["acp"], manifest.local_development.agent.port, agent_env
|
283
|
+
)
|
284
|
+
process_manager.add_process(acp_process)
|
285
|
+
|
286
|
+
# Start output streaming for ACP
|
287
|
+
acp_output_task = asyncio.create_task(stream_process_output(acp_process, "ACP"))
|
288
|
+
|
289
|
+
tasks = [acp_output_task]
|
290
|
+
|
291
|
+
# Start temporal worker if needed
|
292
|
+
if is_temporal_agent(manifest):
|
293
|
+
worker_task = await start_temporal_worker_with_reload(file_paths["worker"], agent_env, process_manager)
|
294
|
+
tasks.append(worker_task)
|
295
|
+
|
296
|
+
console.print(
|
297
|
+
f"\n[green]✓ Agent running at: http://localhost:{manifest.local_development.agent.port}[/green]"
|
298
|
+
)
|
299
|
+
console.print("[dim]Press Ctrl+C to stop[/dim]\n")
|
300
|
+
|
301
|
+
# Wait for shutdown signal or process failure
|
302
|
+
try:
|
303
|
+
await process_manager.wait_for_shutdown()
|
304
|
+
except KeyboardInterrupt:
|
305
|
+
console.print("\n[yellow]Received shutdown signal...[/yellow]")
|
306
|
+
|
307
|
+
# Cancel output streaming tasks
|
308
|
+
for task in tasks:
|
309
|
+
task.cancel()
|
310
|
+
try:
|
311
|
+
await task
|
312
|
+
except asyncio.CancelledError:
|
313
|
+
pass
|
314
|
+
|
315
|
+
except Exception as e:
|
316
|
+
logger.exception("Error running agent")
|
317
|
+
raise RunError(f"Failed to run agent: {str(e)}") from e
|
318
|
+
|
319
|
+
finally:
|
320
|
+
# Ensure cleanup happens
|
321
|
+
await process_manager.cleanup_processes()
|
322
|
+
|
323
|
+
|
324
|
+
def resolve_and_validate_path(base_path: Path, configured_path: str, file_type: str) -> Path:
|
325
|
+
"""Resolve and validate a configured path"""
|
326
|
+
path_obj = Path(configured_path)
|
327
|
+
|
328
|
+
if path_obj.is_absolute():
|
329
|
+
# Absolute path - use as-is
|
330
|
+
resolved_path = path_obj
|
331
|
+
else:
|
332
|
+
# Relative path - resolve relative to manifest directory
|
333
|
+
resolved_path = (base_path / configured_path).resolve()
|
334
|
+
|
335
|
+
# Validate the file exists
|
336
|
+
if not resolved_path.exists():
|
337
|
+
raise RunError(
|
338
|
+
f"{file_type} file not found: {resolved_path}\n"
|
339
|
+
f" Configured path: {configured_path}\n"
|
340
|
+
f" Resolved from manifest: {base_path}"
|
341
|
+
)
|
342
|
+
|
343
|
+
# Validate it's actually a file
|
344
|
+
if not resolved_path.is_file():
|
345
|
+
raise RunError(f"{file_type} path is not a file: {resolved_path}")
|
346
|
+
|
347
|
+
return resolved_path
|
348
|
+
|
349
|
+
|
350
|
+
def validate_path_security(resolved_path: Path, manifest_dir: Path) -> None:
|
351
|
+
"""Basic security validation for resolved paths"""
|
352
|
+
try:
|
353
|
+
# Ensure the resolved path is accessible
|
354
|
+
resolved_path.resolve()
|
355
|
+
|
356
|
+
# Optional: Add warnings for paths that go too far up
|
357
|
+
try:
|
358
|
+
# Check if path goes more than 3 levels up from manifest
|
359
|
+
relative_to_manifest = resolved_path.relative_to(manifest_dir.parent.parent.parent)
|
360
|
+
if str(relative_to_manifest).startswith(".."):
|
361
|
+
logger.warning(
|
362
|
+
f"Path goes significantly outside project structure: {resolved_path}"
|
363
|
+
)
|
364
|
+
except ValueError:
|
365
|
+
# Path is outside the tree - that's okay, just log it
|
366
|
+
logger.info(f"Using path outside manifest directory tree: {resolved_path}")
|
367
|
+
|
368
|
+
except Exception as e:
|
369
|
+
raise RunError(f"Path resolution failed: {resolved_path} - {str(e)}") from e
|
370
|
+
|
371
|
+
|
372
|
+
def get_file_paths(manifest: AgentManifest, manifest_path: str) -> dict[str, Path]:
|
373
|
+
"""Get resolved file paths from manifest configuration"""
|
374
|
+
manifest_dir = Path(manifest_path).parent.resolve()
|
375
|
+
|
376
|
+
# Use configured paths or fall back to defaults for backward compatibility
|
377
|
+
if manifest.local_development and manifest.local_development.paths:
|
378
|
+
paths_config = manifest.local_development.paths
|
379
|
+
|
380
|
+
# Resolve ACP path
|
381
|
+
acp_path = resolve_and_validate_path(manifest_dir, paths_config.acp, "ACP server")
|
382
|
+
validate_path_security(acp_path, manifest_dir)
|
383
|
+
|
384
|
+
# Resolve worker path if specified
|
385
|
+
worker_path = None
|
386
|
+
if paths_config.worker:
|
387
|
+
worker_path = resolve_and_validate_path(
|
388
|
+
manifest_dir, paths_config.worker, "Temporal worker"
|
389
|
+
)
|
390
|
+
validate_path_security(worker_path, manifest_dir)
|
391
|
+
else:
|
392
|
+
# Backward compatibility: use old hardcoded structure
|
393
|
+
project_dir = manifest_dir / "project"
|
394
|
+
acp_path = project_dir / "acp.py"
|
395
|
+
worker_path = project_dir / "run_worker.py" if is_temporal_agent(manifest) else None
|
396
|
+
|
397
|
+
# Validate backward compatibility paths
|
398
|
+
if not acp_path.exists():
|
399
|
+
raise RunError(f"ACP file not found: {acp_path}")
|
400
|
+
|
401
|
+
if worker_path and not worker_path.exists():
|
402
|
+
raise RunError(f"Worker file not found: {worker_path}")
|
403
|
+
|
404
|
+
return {
|
405
|
+
"acp": acp_path,
|
406
|
+
"worker": worker_path,
|
407
|
+
"acp_dir": acp_path.parent,
|
408
|
+
"worker_dir": worker_path.parent if worker_path else None,
|
409
|
+
}
|
410
|
+
|
411
|
+
|
412
|
+
def create_agent_environment(manifest: AgentManifest) -> dict[str, str]:
|
413
|
+
"""Create environment variables for agent processes without modifying os.environ"""
|
414
|
+
# Start with current environment
|
415
|
+
env = dict(os.environ)
|
416
|
+
|
417
|
+
agent_config = manifest.agent
|
418
|
+
|
419
|
+
# TODO: Combine this logic with the deploy_handlers so that we can reuse the env vars
|
420
|
+
env_vars = {
|
421
|
+
"ENVIRONMENT": "development",
|
422
|
+
"TEMPORAL_ADDRESS": "localhost:7233",
|
423
|
+
"REDIS_URL": "redis://localhost:6379",
|
424
|
+
"AGENT_NAME": manifest.agent.name,
|
425
|
+
"ACP_TYPE": manifest.agent.acp_type,
|
426
|
+
"ACP_URL": f"http://{manifest.local_development.agent.host_address}",
|
427
|
+
"ACP_PORT": str(manifest.local_development.agent.port),
|
428
|
+
}
|
429
|
+
|
430
|
+
# Add description if available
|
431
|
+
if manifest.agent.description:
|
432
|
+
env_vars["AGENT_DESCRIPTION"] = manifest.agent.description
|
433
|
+
|
434
|
+
# Add temporal-specific variables if this is a temporal agent
|
435
|
+
if manifest.agent.is_temporal_agent():
|
436
|
+
temporal_config = manifest.agent.get_temporal_workflow_config()
|
437
|
+
if temporal_config:
|
438
|
+
env_vars["WORKFLOW_NAME"] = temporal_config.name
|
439
|
+
env_vars["WORKFLOW_TASK_QUEUE"] = temporal_config.queue_name
|
440
|
+
|
441
|
+
if agent_config.env:
|
442
|
+
for key, value in agent_config.env.items():
|
443
|
+
env_vars[key] = value
|
444
|
+
|
445
|
+
env.update(env_vars)
|
446
|
+
|
447
|
+
return env
|
448
|
+
|
449
|
+
|
450
|
+
def is_temporal_agent(manifest: AgentManifest) -> bool:
|
451
|
+
"""Check if this is a temporal agent"""
|
452
|
+
return manifest.agent.is_temporal_agent()
|