steerdev 0.4.27__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.
- steerdev-0.4.27.dist-info/METADATA +224 -0
- steerdev-0.4.27.dist-info/RECORD +57 -0
- steerdev-0.4.27.dist-info/WHEEL +4 -0
- steerdev-0.4.27.dist-info/entry_points.txt +2 -0
- steerdev_agent/__init__.py +10 -0
- steerdev_agent/api/__init__.py +32 -0
- steerdev_agent/api/activity.py +278 -0
- steerdev_agent/api/agents.py +145 -0
- steerdev_agent/api/client.py +158 -0
- steerdev_agent/api/commands.py +399 -0
- steerdev_agent/api/configs.py +238 -0
- steerdev_agent/api/context.py +306 -0
- steerdev_agent/api/events.py +294 -0
- steerdev_agent/api/hooks.py +178 -0
- steerdev_agent/api/implementation_plan.py +408 -0
- steerdev_agent/api/messages.py +231 -0
- steerdev_agent/api/prd.py +281 -0
- steerdev_agent/api/runs.py +526 -0
- steerdev_agent/api/sessions.py +403 -0
- steerdev_agent/api/specs.py +321 -0
- steerdev_agent/api/tasks.py +659 -0
- steerdev_agent/api/workflow_runs.py +351 -0
- steerdev_agent/api/workflows.py +191 -0
- steerdev_agent/cli.py +2254 -0
- steerdev_agent/config/__init__.py +19 -0
- steerdev_agent/config/models.py +236 -0
- steerdev_agent/config/platform.py +272 -0
- steerdev_agent/config/settings.py +62 -0
- steerdev_agent/daemon.py +675 -0
- steerdev_agent/executor/__init__.py +64 -0
- steerdev_agent/executor/base.py +121 -0
- steerdev_agent/executor/claude.py +328 -0
- steerdev_agent/executor/stream.py +163 -0
- steerdev_agent/git/__init__.py +1 -0
- steerdev_agent/handlers/__init__.py +5 -0
- steerdev_agent/handlers/prd.py +533 -0
- steerdev_agent/integration.py +334 -0
- steerdev_agent/prompt/__init__.py +10 -0
- steerdev_agent/prompt/builder.py +263 -0
- steerdev_agent/prompt/templates.py +422 -0
- steerdev_agent/py.typed +0 -0
- steerdev_agent/runner.py +829 -0
- steerdev_agent/setup/__init__.py +5 -0
- steerdev_agent/setup/claude_setup.py +560 -0
- steerdev_agent/setup/templates/claude_md_section.md +140 -0
- steerdev_agent/setup/templates/settings.json +69 -0
- steerdev_agent/setup/templates/skills/activity/SKILL.md +160 -0
- steerdev_agent/setup/templates/skills/context/SKILL.md +122 -0
- steerdev_agent/setup/templates/skills/git-workflow/SKILL.md +218 -0
- steerdev_agent/setup/templates/skills/progress-logging/SKILL.md +211 -0
- steerdev_agent/setup/templates/skills/specs-management/SKILL.md +161 -0
- steerdev_agent/setup/templates/skills/task-management/SKILL.md +343 -0
- steerdev_agent/setup/templates/steerdev.yaml +51 -0
- steerdev_agent/version.py +149 -0
- steerdev_agent/workflow/__init__.py +10 -0
- steerdev_agent/workflow/executor.py +494 -0
- steerdev_agent/workflow/memory.py +185 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Agent registration API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Self
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from steerdev_agent.api.client import get_api_endpoint, get_api_key
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentRegisterRequest(BaseModel):
|
|
15
|
+
"""Request model for registering an agent."""
|
|
16
|
+
|
|
17
|
+
name: str = Field(description="Agent name for identification")
|
|
18
|
+
application: str | None = Field(
|
|
19
|
+
default=None, description="Application type (e.g., claude_code)"
|
|
20
|
+
)
|
|
21
|
+
hostname: str | None = Field(default=None, description="Hostname where agent is running")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AgentResponse(BaseModel):
|
|
25
|
+
"""Response model for agent data."""
|
|
26
|
+
|
|
27
|
+
id: str
|
|
28
|
+
project_id: str
|
|
29
|
+
name: str | None = None
|
|
30
|
+
application: str | None = None
|
|
31
|
+
hostname: str | None = None
|
|
32
|
+
status: str
|
|
33
|
+
last_heartbeat_at: str | None = None
|
|
34
|
+
created_at: str
|
|
35
|
+
updated_at: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AgentsClient:
|
|
39
|
+
"""Async HTTP client for Agent registration API.
|
|
40
|
+
|
|
41
|
+
Allows agents to register themselves and get their agent_id.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, api_key: str | None = None, timeout: float = 30.0) -> None:
|
|
45
|
+
"""Initialize the client.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
api_key: API key for authentication. If not provided, reads from STEERDEV_API_KEY.
|
|
49
|
+
timeout: Request timeout in seconds.
|
|
50
|
+
"""
|
|
51
|
+
self.api_key = api_key or get_api_key()
|
|
52
|
+
self.api_base = get_api_endpoint()
|
|
53
|
+
self.timeout = timeout
|
|
54
|
+
self._client: httpx.AsyncClient | None = None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def headers(self) -> dict[str, str]:
|
|
58
|
+
"""Get request headers with authentication."""
|
|
59
|
+
return {
|
|
60
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
61
|
+
"Content-Type": "application/json",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
65
|
+
"""Get or create async HTTP client."""
|
|
66
|
+
if self._client is None:
|
|
67
|
+
self._client = httpx.AsyncClient(timeout=self.timeout)
|
|
68
|
+
return self._client
|
|
69
|
+
|
|
70
|
+
async def close(self) -> None:
|
|
71
|
+
"""Close the HTTP client."""
|
|
72
|
+
if self._client is not None:
|
|
73
|
+
await self._client.aclose()
|
|
74
|
+
self._client = None
|
|
75
|
+
|
|
76
|
+
async def __aenter__(self) -> Self:
|
|
77
|
+
"""Enter async context manager."""
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
81
|
+
"""Exit async context manager."""
|
|
82
|
+
await self.close()
|
|
83
|
+
|
|
84
|
+
async def register(self, request: AgentRegisterRequest) -> AgentResponse | None:
|
|
85
|
+
"""Register an agent or get existing one.
|
|
86
|
+
|
|
87
|
+
This is idempotent - calling with the same name will return the existing agent.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
request: Agent registration request with name and optional metadata.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Agent data including the agent_id, or None on failure.
|
|
94
|
+
"""
|
|
95
|
+
client = await self._get_client()
|
|
96
|
+
logger.debug(f"Registering agent '{request.name}' at {self.api_base}/agents")
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
response = await client.post(
|
|
100
|
+
f"{self.api_base}/agents",
|
|
101
|
+
headers=self.headers,
|
|
102
|
+
json=request.model_dump(exclude_none=True),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if response.status_code in (200, 201):
|
|
106
|
+
data = response.json()
|
|
107
|
+
logger.info(f"Agent registered successfully with ID: {data.get('id')}")
|
|
108
|
+
return AgentResponse(**data)
|
|
109
|
+
|
|
110
|
+
logger.error(f"Failed to register agent: {response.status_code} - {response.text}")
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
except httpx.RequestError as e:
|
|
114
|
+
logger.error(f"Request error registering agent: {e}")
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
async def list_agents(self, limit: int = 50, offset: int = 0) -> list[AgentResponse]:
|
|
118
|
+
"""List agents for the project.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
limit: Maximum number of agents to return.
|
|
122
|
+
offset: Offset for pagination.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
List of agent data.
|
|
126
|
+
"""
|
|
127
|
+
client = await self._get_client()
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
response = await client.get(
|
|
131
|
+
f"{self.api_base}/agents",
|
|
132
|
+
headers=self.headers,
|
|
133
|
+
params={"limit": limit, "offset": offset},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if response.status_code == 200:
|
|
137
|
+
data = response.json()
|
|
138
|
+
return [AgentResponse(**agent) for agent in data.get("agents", [])]
|
|
139
|
+
|
|
140
|
+
logger.error(f"Failed to list agents: {response.status_code} - {response.text}")
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
except httpx.RequestError as e:
|
|
144
|
+
logger.error(f"Request error listing agents: {e}")
|
|
145
|
+
return []
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Base API client for SteerDev API."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Self
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
# Default API endpoint
|
|
12
|
+
DEFAULT_API_ENDPOINT = "https://steerdev.com/api/v1"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_api_key() -> str | None:
|
|
16
|
+
"""Get API key from environment."""
|
|
17
|
+
return os.environ.get("STEERDEV_API_KEY")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_project_id() -> str | None:
|
|
21
|
+
"""Get project ID from environment."""
|
|
22
|
+
return os.environ.get("STEERDEV_PROJECT_ID")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_agent_id() -> str | None:
|
|
26
|
+
"""Get agent ID from environment."""
|
|
27
|
+
return os.environ.get("STEERDEV_AGENT_ID")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_agent_name() -> str | None:
|
|
31
|
+
"""Get agent name from environment."""
|
|
32
|
+
return os.environ.get("STEERDEV_AGENT_NAME")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_api_endpoint() -> str:
|
|
36
|
+
"""Get API endpoint from environment or use default."""
|
|
37
|
+
return os.environ.get("STEERDEV_API_ENDPOINT", DEFAULT_API_ENDPOINT)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SteerDevClient:
|
|
41
|
+
"""Base HTTP client for SteerDev API.
|
|
42
|
+
|
|
43
|
+
Handles authentication, headers, and common error handling.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, api_key: str | None = None, timeout: float = 30.0) -> None:
|
|
47
|
+
"""Initialize the client.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
api_key: API key for authentication. If not provided, reads from STEERDEV_API_KEY env var.
|
|
51
|
+
timeout: Request timeout in seconds.
|
|
52
|
+
"""
|
|
53
|
+
self.api_key = api_key or get_api_key()
|
|
54
|
+
self.api_base = get_api_endpoint()
|
|
55
|
+
self.timeout = timeout
|
|
56
|
+
self._client: httpx.Client | None = None
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def headers(self) -> dict[str, str]:
|
|
60
|
+
"""Get request headers with authentication."""
|
|
61
|
+
return {
|
|
62
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def _get_client(self) -> httpx.Client:
|
|
67
|
+
"""Get or create HTTP client."""
|
|
68
|
+
if self._client is None:
|
|
69
|
+
self._client = httpx.Client(timeout=self.timeout, follow_redirects=True)
|
|
70
|
+
return self._client
|
|
71
|
+
|
|
72
|
+
def close(self) -> None:
|
|
73
|
+
"""Close the HTTP client."""
|
|
74
|
+
if self._client is not None:
|
|
75
|
+
self._client.close()
|
|
76
|
+
self._client = None
|
|
77
|
+
|
|
78
|
+
def __enter__(self) -> Self:
|
|
79
|
+
"""Enter context manager."""
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
83
|
+
"""Exit context manager."""
|
|
84
|
+
self.close()
|
|
85
|
+
|
|
86
|
+
def get(
|
|
87
|
+
self,
|
|
88
|
+
path: str,
|
|
89
|
+
params: dict[str, Any] | None = None,
|
|
90
|
+
) -> httpx.Response:
|
|
91
|
+
"""Make a GET request.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
path: API path (e.g., "/tasks").
|
|
95
|
+
params: Query parameters.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
HTTP response.
|
|
99
|
+
"""
|
|
100
|
+
client = self._get_client()
|
|
101
|
+
return client.get(
|
|
102
|
+
f"{self.api_base}{path}",
|
|
103
|
+
headers=self.headers,
|
|
104
|
+
params=params,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def post(
|
|
108
|
+
self,
|
|
109
|
+
path: str,
|
|
110
|
+
json: dict[str, Any] | None = None,
|
|
111
|
+
) -> httpx.Response:
|
|
112
|
+
"""Make a POST request.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
path: API path (e.g., "/tasks").
|
|
116
|
+
json: Request body as JSON.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
HTTP response.
|
|
120
|
+
"""
|
|
121
|
+
client = self._get_client()
|
|
122
|
+
return client.post(
|
|
123
|
+
f"{self.api_base}{path}",
|
|
124
|
+
headers=self.headers,
|
|
125
|
+
json=json,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def patch(
|
|
129
|
+
self,
|
|
130
|
+
path: str,
|
|
131
|
+
json: dict[str, Any] | None = None,
|
|
132
|
+
) -> httpx.Response:
|
|
133
|
+
"""Make a PATCH request.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
path: API path (e.g., "/tasks/{id}").
|
|
137
|
+
json: Request body as JSON.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
HTTP response.
|
|
141
|
+
"""
|
|
142
|
+
client = self._get_client()
|
|
143
|
+
return client.patch(
|
|
144
|
+
f"{self.api_base}{path}",
|
|
145
|
+
headers=self.headers,
|
|
146
|
+
json=json,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def check_api_key(self) -> bool:
|
|
150
|
+
"""Check if API key is configured.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
True if API key is set.
|
|
154
|
+
"""
|
|
155
|
+
if not self.api_key:
|
|
156
|
+
console.print("[red]Error: STEERDEV_API_KEY environment variable not set[/red]")
|
|
157
|
+
return False
|
|
158
|
+
return True
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Commands API client for agent daemon mode."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal, Self
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from steerdev_agent.api.client import get_agent_id, get_api_endpoint, get_api_key
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
CommandType = Literal["task", "prompt", "control"]
|
|
17
|
+
CommandStatus = Literal[
|
|
18
|
+
"pending", "claimed", "executing", "completed", "failed", "cancelled", "expired"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
STATUS_STYLES: dict[str, str] = {
|
|
22
|
+
"pending": "dim",
|
|
23
|
+
"claimed": "yellow",
|
|
24
|
+
"executing": "blue",
|
|
25
|
+
"completed": "green",
|
|
26
|
+
"failed": "red",
|
|
27
|
+
"cancelled": "dim",
|
|
28
|
+
"expired": "dim",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CommandResponse(BaseModel):
|
|
33
|
+
"""Response model for command data."""
|
|
34
|
+
|
|
35
|
+
id: str
|
|
36
|
+
agent_id: str
|
|
37
|
+
project_id: str
|
|
38
|
+
command_type: CommandType
|
|
39
|
+
task_id: str | None = None
|
|
40
|
+
prompt: str | None = None
|
|
41
|
+
control_action: str | None = None
|
|
42
|
+
working_directory: str | None = None
|
|
43
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
44
|
+
status: CommandStatus
|
|
45
|
+
priority: int = 0
|
|
46
|
+
session_id: str | None = None
|
|
47
|
+
result: str | None = None
|
|
48
|
+
error: str | None = None
|
|
49
|
+
claimed_at: str | None = None
|
|
50
|
+
started_at: str | None = None
|
|
51
|
+
completed_at: str | None = None
|
|
52
|
+
expires_at: str | None = None
|
|
53
|
+
submitted_by: str | None = None
|
|
54
|
+
created_at: str
|
|
55
|
+
updated_at: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CommandListResponse(BaseModel):
|
|
59
|
+
"""Response model for listing commands."""
|
|
60
|
+
|
|
61
|
+
commands: list[CommandResponse]
|
|
62
|
+
total: int
|
|
63
|
+
limit: int
|
|
64
|
+
offset: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CommandsClient:
|
|
68
|
+
"""Async HTTP client for the agent commands API.
|
|
69
|
+
|
|
70
|
+
Used by the daemon to poll for commands, claim them, and report results.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
agent_id: str | None = None,
|
|
76
|
+
api_key: str | None = None,
|
|
77
|
+
timeout: float = 30.0,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Initialize the client.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
agent_id: Agent ID for API calls. If not provided, reads from STEERDEV_AGENT_ID.
|
|
83
|
+
api_key: API key for authentication. If not provided, reads from STEERDEV_API_KEY.
|
|
84
|
+
timeout: Request timeout in seconds.
|
|
85
|
+
"""
|
|
86
|
+
self.agent_id = agent_id or get_agent_id()
|
|
87
|
+
self.api_key = api_key or get_api_key()
|
|
88
|
+
self.api_base = get_api_endpoint()
|
|
89
|
+
self.timeout = timeout
|
|
90
|
+
self._client: httpx.AsyncClient | None = None
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def _base_url(self) -> str:
|
|
94
|
+
"""Get the base URL for commands endpoints."""
|
|
95
|
+
return f"{self.api_base}/agents/{self.agent_id}/commands"
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def headers(self) -> dict[str, str]:
|
|
99
|
+
"""Get request headers with authentication."""
|
|
100
|
+
return {
|
|
101
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
106
|
+
"""Get or create async HTTP client."""
|
|
107
|
+
if self._client is None:
|
|
108
|
+
self._client = httpx.AsyncClient(timeout=self.timeout)
|
|
109
|
+
return self._client
|
|
110
|
+
|
|
111
|
+
async def close(self) -> None:
|
|
112
|
+
"""Close the HTTP client."""
|
|
113
|
+
if self._client is not None:
|
|
114
|
+
await self._client.aclose()
|
|
115
|
+
self._client = None
|
|
116
|
+
|
|
117
|
+
async def __aenter__(self) -> Self:
|
|
118
|
+
"""Enter async context manager."""
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
122
|
+
"""Exit async context manager."""
|
|
123
|
+
await self.close()
|
|
124
|
+
|
|
125
|
+
async def poll_next(self) -> CommandResponse | None:
|
|
126
|
+
"""Poll for and atomically claim the next pending command.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Claimed command or None if queue is empty.
|
|
130
|
+
"""
|
|
131
|
+
client = await self._get_client()
|
|
132
|
+
logger.debug(f"Polling for next command at {self._base_url}/next")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
response = await client.get(
|
|
136
|
+
f"{self._base_url}/next",
|
|
137
|
+
headers=self.headers,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if response.status_code == 200:
|
|
141
|
+
return CommandResponse(**response.json())
|
|
142
|
+
if response.status_code == 204:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
logger.error(f"Failed to poll next command: {response.status_code} - {response.text}")
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
except httpx.RequestError as e:
|
|
149
|
+
logger.error(f"Request error polling next command: {e}")
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
async def update_command(
|
|
153
|
+
self,
|
|
154
|
+
command_id: str,
|
|
155
|
+
status: CommandStatus | None = None,
|
|
156
|
+
session_id: str | None = None,
|
|
157
|
+
result: str | None = None,
|
|
158
|
+
error: str | None = None,
|
|
159
|
+
started_at: str | None = None,
|
|
160
|
+
completed_at: str | None = None,
|
|
161
|
+
) -> CommandResponse | None:
|
|
162
|
+
"""Update a command's status and metadata.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
command_id: Command ID to update.
|
|
166
|
+
status: New status.
|
|
167
|
+
session_id: Session ID created for this command.
|
|
168
|
+
result: Result summary.
|
|
169
|
+
error: Error message.
|
|
170
|
+
started_at: Execution start timestamp.
|
|
171
|
+
completed_at: Completion timestamp.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Updated command or None on failure.
|
|
175
|
+
"""
|
|
176
|
+
client = await self._get_client()
|
|
177
|
+
|
|
178
|
+
payload: dict[str, Any] = {}
|
|
179
|
+
if status is not None:
|
|
180
|
+
payload["status"] = status
|
|
181
|
+
if session_id is not None:
|
|
182
|
+
payload["session_id"] = session_id
|
|
183
|
+
if result is not None:
|
|
184
|
+
payload["result"] = result
|
|
185
|
+
if error is not None:
|
|
186
|
+
payload["error"] = error
|
|
187
|
+
if started_at is not None:
|
|
188
|
+
payload["started_at"] = started_at
|
|
189
|
+
if completed_at is not None:
|
|
190
|
+
payload["completed_at"] = completed_at
|
|
191
|
+
|
|
192
|
+
if not payload:
|
|
193
|
+
logger.warning("No fields to update")
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
response = await client.patch(
|
|
198
|
+
f"{self._base_url}/{command_id}",
|
|
199
|
+
headers=self.headers,
|
|
200
|
+
json=payload,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if response.status_code == 200:
|
|
204
|
+
return CommandResponse(**response.json())
|
|
205
|
+
|
|
206
|
+
logger.error(f"Failed to update command: {response.status_code} - {response.text}")
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
except httpx.RequestError as e:
|
|
210
|
+
logger.error(f"Request error updating command: {e}")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
async def mark_executing(self, command_id: str, session_id: str) -> CommandResponse | None:
|
|
214
|
+
"""Mark a command as executing with its session ID.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
command_id: Command ID.
|
|
218
|
+
session_id: Session ID created for execution.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Updated command or None on failure.
|
|
222
|
+
"""
|
|
223
|
+
from datetime import UTC, datetime
|
|
224
|
+
|
|
225
|
+
return await self.update_command(
|
|
226
|
+
command_id,
|
|
227
|
+
status="executing",
|
|
228
|
+
session_id=session_id,
|
|
229
|
+
started_at=datetime.now(UTC).isoformat(),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
async def mark_completed(
|
|
233
|
+
self, command_id: str, result: str | None = None
|
|
234
|
+
) -> CommandResponse | None:
|
|
235
|
+
"""Mark a command as completed.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
command_id: Command ID.
|
|
239
|
+
result: Result summary.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Updated command or None on failure.
|
|
243
|
+
"""
|
|
244
|
+
from datetime import UTC, datetime
|
|
245
|
+
|
|
246
|
+
return await self.update_command(
|
|
247
|
+
command_id,
|
|
248
|
+
status="completed",
|
|
249
|
+
result=result,
|
|
250
|
+
completed_at=datetime.now(UTC).isoformat(),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
async def mark_failed(
|
|
254
|
+
self, command_id: str, error: str | None = None
|
|
255
|
+
) -> CommandResponse | None:
|
|
256
|
+
"""Mark a command as failed.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
command_id: Command ID.
|
|
260
|
+
error: Error message.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Updated command or None on failure.
|
|
264
|
+
"""
|
|
265
|
+
from datetime import UTC, datetime
|
|
266
|
+
|
|
267
|
+
return await self.update_command(
|
|
268
|
+
command_id,
|
|
269
|
+
status="failed",
|
|
270
|
+
error=error,
|
|
271
|
+
completed_at=datetime.now(UTC).isoformat(),
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
async def get_command(self, command_id: str) -> CommandResponse | None:
|
|
275
|
+
"""Get a single command by ID.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
command_id: Command ID.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Command data or None if not found.
|
|
282
|
+
"""
|
|
283
|
+
client = await self._get_client()
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
response = await client.get(
|
|
287
|
+
f"{self._base_url}/{command_id}",
|
|
288
|
+
headers=self.headers,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if response.status_code == 200:
|
|
292
|
+
return CommandResponse(**response.json())
|
|
293
|
+
if response.status_code == 404:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
logger.error(f"Failed to get command: {response.status_code} - {response.text}")
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
except httpx.RequestError as e:
|
|
300
|
+
logger.error(f"Request error getting command: {e}")
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
async def list_commands(
|
|
304
|
+
self,
|
|
305
|
+
status: CommandStatus | None = None,
|
|
306
|
+
limit: int = 50,
|
|
307
|
+
offset: int = 0,
|
|
308
|
+
) -> CommandListResponse | None:
|
|
309
|
+
"""List commands for this agent.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
status: Filter by status.
|
|
313
|
+
limit: Maximum results.
|
|
314
|
+
offset: Pagination offset.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Command list response or None on failure.
|
|
318
|
+
"""
|
|
319
|
+
client = await self._get_client()
|
|
320
|
+
params: dict[str, str | int] = {"limit": limit, "offset": offset}
|
|
321
|
+
if status:
|
|
322
|
+
params["status"] = status
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
response = await client.get(
|
|
326
|
+
self._base_url,
|
|
327
|
+
headers=self.headers,
|
|
328
|
+
params=params,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if response.status_code == 200:
|
|
332
|
+
return CommandListResponse(**response.json())
|
|
333
|
+
|
|
334
|
+
logger.error(f"Failed to list commands: {response.status_code} - {response.text}")
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
except httpx.RequestError as e:
|
|
338
|
+
logger.error(f"Request error listing commands: {e}")
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def display_command(command: CommandResponse, title: str = "Command") -> None:
|
|
343
|
+
"""Display a command in a formatted panel."""
|
|
344
|
+
status_style = STATUS_STYLES.get(command.status, "white")
|
|
345
|
+
|
|
346
|
+
info = (
|
|
347
|
+
f"[bold cyan]ID:[/bold cyan] {command.id}\n"
|
|
348
|
+
f"[bold cyan]Type:[/bold cyan] {command.command_type}\n"
|
|
349
|
+
f"[bold cyan]Status:[/bold cyan] [{status_style}]{command.status}[/{status_style}]\n"
|
|
350
|
+
f"[bold cyan]Priority:[/bold cyan] {command.priority}\n"
|
|
351
|
+
f"[bold cyan]Created:[/bold cyan] {command.created_at}\n"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if command.task_id:
|
|
355
|
+
info += f"[bold cyan]Task ID:[/bold cyan] {command.task_id}\n"
|
|
356
|
+
if command.prompt:
|
|
357
|
+
prompt_display = (
|
|
358
|
+
command.prompt[:200] + "..." if len(command.prompt) > 200 else command.prompt
|
|
359
|
+
)
|
|
360
|
+
info += f"[bold cyan]Prompt:[/bold cyan] {prompt_display}\n"
|
|
361
|
+
if command.control_action:
|
|
362
|
+
info += f"[bold cyan]Control Action:[/bold cyan] {command.control_action}\n"
|
|
363
|
+
if command.session_id:
|
|
364
|
+
info += f"[bold cyan]Session ID:[/bold cyan] {command.session_id}\n"
|
|
365
|
+
if command.result:
|
|
366
|
+
info += f"[bold cyan]Result:[/bold cyan] {command.result}\n"
|
|
367
|
+
if command.error:
|
|
368
|
+
info += f"[bold cyan]Error:[/bold cyan] [red]{command.error}[/red]\n"
|
|
369
|
+
|
|
370
|
+
console.print(Panel(info, title=title, border_style="green"))
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def display_command_list(commands: list[CommandResponse], full_ids: bool = True) -> None:
|
|
374
|
+
"""Display a list of commands in a formatted table."""
|
|
375
|
+
if not commands:
|
|
376
|
+
console.print("[yellow]No commands found[/yellow]")
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
table = Table(title="Agent Commands")
|
|
380
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
381
|
+
table.add_column("Type", style="white")
|
|
382
|
+
table.add_column("Status", style="magenta")
|
|
383
|
+
table.add_column("Priority", style="yellow")
|
|
384
|
+
table.add_column("Created", style="green")
|
|
385
|
+
|
|
386
|
+
for cmd in commands:
|
|
387
|
+
status_style = STATUS_STYLES.get(cmd.status, "white")
|
|
388
|
+
cmd_id = cmd.id if full_ids else (cmd.id[:8] + "..." if len(cmd.id) > 8 else cmd.id)
|
|
389
|
+
|
|
390
|
+
table.add_row(
|
|
391
|
+
cmd_id,
|
|
392
|
+
cmd.command_type,
|
|
393
|
+
f"[{status_style}]{cmd.status}[/{status_style}]",
|
|
394
|
+
str(cmd.priority),
|
|
395
|
+
cmd.created_at[:19] if cmd.created_at else "-",
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
console.print(table)
|
|
399
|
+
console.print(f"\n[dim]Total: {len(commands)} commands[/dim]")
|