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,526 @@
|
|
|
1
|
+
"""Runs management API client for tracking agent execution sessions."""
|
|
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_api_endpoint, get_api_key
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
RunStatus = Literal["pending", "running", "completed", "failed", "cancelled"]
|
|
17
|
+
|
|
18
|
+
# Status display styles used by display functions
|
|
19
|
+
STATUS_STYLES: dict[str, str] = {
|
|
20
|
+
"pending": "dim",
|
|
21
|
+
"running": "yellow",
|
|
22
|
+
"completed": "green",
|
|
23
|
+
"failed": "red",
|
|
24
|
+
"cancelled": "dim",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RunCreateRequest(BaseModel):
|
|
29
|
+
"""Request model for creating a run."""
|
|
30
|
+
|
|
31
|
+
run_id: str = Field(description="Unique run identifier (UUID)")
|
|
32
|
+
session_name: str = Field(description="Session name for the run")
|
|
33
|
+
agent_id: str | None = Field(default=None, description="Agent ID executing this run")
|
|
34
|
+
task_id: str | None = Field(default=None, description="Task being executed")
|
|
35
|
+
working_directory: str | None = Field(default=None, description="Working directory")
|
|
36
|
+
application: str | None = Field(
|
|
37
|
+
default=None, description="Application type (e.g., claude_code)"
|
|
38
|
+
)
|
|
39
|
+
initial_prompt: str | None = Field(default=None, description="Initial prompt sent to agent")
|
|
40
|
+
metadata: dict[str, Any] | None = Field(default=None, description="Additional metadata")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RunResponse(BaseModel):
|
|
44
|
+
"""Response model for run data."""
|
|
45
|
+
|
|
46
|
+
id: str
|
|
47
|
+
session_name: str
|
|
48
|
+
api_key_id: str | None = None
|
|
49
|
+
agent_id: str | None = None
|
|
50
|
+
task_id: str | None = None
|
|
51
|
+
status: RunStatus
|
|
52
|
+
working_directory: str | None = None
|
|
53
|
+
application: str | None = None
|
|
54
|
+
initial_prompt: str | None = None
|
|
55
|
+
started_at: str | None = None
|
|
56
|
+
completed_at: str | None = None
|
|
57
|
+
duration_seconds: float | None = None
|
|
58
|
+
tasks_executed: int = 0
|
|
59
|
+
tasks_succeeded: int = 0
|
|
60
|
+
tasks_failed: int = 0
|
|
61
|
+
error_message: str | None = None
|
|
62
|
+
error_type: str | None = None
|
|
63
|
+
metadata: dict[str, Any] | None = None
|
|
64
|
+
last_heartbeat_at: str | None = None
|
|
65
|
+
# Observability fields
|
|
66
|
+
current_task_id: str | None = None
|
|
67
|
+
current_task_title: str | None = None
|
|
68
|
+
tmux_session: str | None = None
|
|
69
|
+
tmux_socket: str | None = None
|
|
70
|
+
agent_host: str | None = None
|
|
71
|
+
created_at: str
|
|
72
|
+
updated_at: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class RunListResponse(BaseModel):
|
|
76
|
+
"""Response model for listing runs."""
|
|
77
|
+
|
|
78
|
+
runs: list[RunResponse]
|
|
79
|
+
total: int
|
|
80
|
+
limit: int
|
|
81
|
+
offset: int
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ActiveRunsResponse(BaseModel):
|
|
85
|
+
"""Response model for active runs."""
|
|
86
|
+
|
|
87
|
+
runs: list[RunResponse]
|
|
88
|
+
count: int
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class RunsClient:
|
|
92
|
+
"""Async HTTP client for Runs API lifecycle management.
|
|
93
|
+
|
|
94
|
+
Manages the full run lifecycle: create, heartbeat, complete, fail.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, api_key: str | None = None, timeout: float = 30.0) -> None:
|
|
98
|
+
"""Initialize the client.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
api_key: API key for authentication. If not provided, reads from STEERDEV_API_KEY.
|
|
102
|
+
timeout: Request timeout in seconds.
|
|
103
|
+
"""
|
|
104
|
+
self.api_key = api_key or get_api_key()
|
|
105
|
+
self.api_base = get_api_endpoint()
|
|
106
|
+
self.timeout = timeout
|
|
107
|
+
self._client: httpx.AsyncClient | None = None
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def headers(self) -> dict[str, str]:
|
|
111
|
+
"""Get request headers with authentication."""
|
|
112
|
+
return {
|
|
113
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
118
|
+
"""Get or create async HTTP client."""
|
|
119
|
+
if self._client is None:
|
|
120
|
+
self._client = httpx.AsyncClient(timeout=self.timeout)
|
|
121
|
+
return self._client
|
|
122
|
+
|
|
123
|
+
async def close(self) -> None:
|
|
124
|
+
"""Close the HTTP client."""
|
|
125
|
+
if self._client is not None:
|
|
126
|
+
await self._client.aclose()
|
|
127
|
+
self._client = None
|
|
128
|
+
|
|
129
|
+
async def __aenter__(self) -> Self:
|
|
130
|
+
"""Enter async context manager."""
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
134
|
+
"""Exit async context manager."""
|
|
135
|
+
await self.close()
|
|
136
|
+
|
|
137
|
+
async def create_run(self, request: RunCreateRequest) -> RunResponse | None:
|
|
138
|
+
"""Create a new run.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
request: Run creation request with run_id and session_name.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Created run data or None on failure.
|
|
145
|
+
"""
|
|
146
|
+
client = await self._get_client()
|
|
147
|
+
logger.debug(f"Creating run {request.run_id} at {self.api_base}/runs")
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
response = await client.post(
|
|
151
|
+
f"{self.api_base}/runs",
|
|
152
|
+
headers=self.headers,
|
|
153
|
+
json=request.model_dump(exclude_none=True),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if response.status_code == 201:
|
|
157
|
+
return RunResponse(**response.json())
|
|
158
|
+
|
|
159
|
+
logger.error(f"Failed to create run: {response.status_code} - {response.text}")
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
except httpx.RequestError as e:
|
|
163
|
+
logger.error(f"Request error creating run: {e}")
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
async def get_run(self, run_id: str) -> RunResponse | None:
|
|
167
|
+
"""Get a specific run by ID.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
run_id: Run ID (UUID) to fetch.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Run data or None if not found.
|
|
174
|
+
"""
|
|
175
|
+
client = await self._get_client()
|
|
176
|
+
logger.debug(f"Fetching run {run_id}")
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
response = await client.get(
|
|
180
|
+
f"{self.api_base}/runs/{run_id}",
|
|
181
|
+
headers=self.headers,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if response.status_code == 200:
|
|
185
|
+
return RunResponse(**response.json())
|
|
186
|
+
if response.status_code == 404:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
logger.error(f"Failed to get run: {response.status_code} - {response.text}")
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
except httpx.RequestError as e:
|
|
193
|
+
logger.error(f"Request error getting run: {e}")
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
async def list_runs(
|
|
197
|
+
self,
|
|
198
|
+
status: RunStatus | None = None,
|
|
199
|
+
limit: int = 50,
|
|
200
|
+
offset: int = 0,
|
|
201
|
+
) -> RunListResponse | None:
|
|
202
|
+
"""List runs with optional filters.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
status: Filter by run status.
|
|
206
|
+
limit: Maximum number of runs to return.
|
|
207
|
+
offset: Offset for pagination.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Run list response or None on failure.
|
|
211
|
+
"""
|
|
212
|
+
client = await self._get_client()
|
|
213
|
+
params: dict[str, str | int] = {"limit": limit, "offset": offset}
|
|
214
|
+
if status:
|
|
215
|
+
params["status"] = status
|
|
216
|
+
|
|
217
|
+
logger.debug(f"Listing runs with params: {params}")
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
response = await client.get(
|
|
221
|
+
f"{self.api_base}/runs",
|
|
222
|
+
headers=self.headers,
|
|
223
|
+
params=params,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if response.status_code == 200:
|
|
227
|
+
return RunListResponse(**response.json())
|
|
228
|
+
|
|
229
|
+
logger.error(f"Failed to list runs: {response.status_code} - {response.text}")
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
except httpx.RequestError as e:
|
|
233
|
+
logger.error(f"Request error listing runs: {e}")
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
async def get_active_runs(self) -> ActiveRunsResponse | None:
|
|
237
|
+
"""Get currently active runs (pending or running).
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Active runs response or None on failure.
|
|
241
|
+
"""
|
|
242
|
+
client = await self._get_client()
|
|
243
|
+
logger.debug("Fetching active runs")
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
response = await client.get(
|
|
247
|
+
f"{self.api_base}/runs/active",
|
|
248
|
+
headers=self.headers,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if response.status_code == 200:
|
|
252
|
+
return ActiveRunsResponse(**response.json())
|
|
253
|
+
|
|
254
|
+
logger.error(f"Failed to get active runs: {response.status_code} - {response.text}")
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
except httpx.RequestError as e:
|
|
258
|
+
logger.error(f"Request error getting active runs: {e}")
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
async def send_heartbeat(
|
|
262
|
+
self,
|
|
263
|
+
run_id: str,
|
|
264
|
+
status: RunStatus | None = None,
|
|
265
|
+
tasks_executed: int | None = None,
|
|
266
|
+
tasks_succeeded: int | None = None,
|
|
267
|
+
tasks_failed: int | None = None,
|
|
268
|
+
current_task_id: str | None = None,
|
|
269
|
+
current_task_title: str | None = None,
|
|
270
|
+
tmux_session: str | None = None,
|
|
271
|
+
tmux_socket: str | None = None,
|
|
272
|
+
agent_host: str | None = None,
|
|
273
|
+
metadata: dict[str, Any] | None = None,
|
|
274
|
+
) -> RunResponse | None:
|
|
275
|
+
"""Send a heartbeat to indicate the run is still alive.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
run_id: Run ID to send heartbeat for.
|
|
279
|
+
status: Current run status.
|
|
280
|
+
tasks_executed: Updated tasks executed count.
|
|
281
|
+
tasks_succeeded: Updated tasks succeeded count.
|
|
282
|
+
tasks_failed: Updated tasks failed count.
|
|
283
|
+
current_task_id: ID of the task currently being worked on.
|
|
284
|
+
current_task_title: Title of the task currently being worked on.
|
|
285
|
+
tmux_session: Tmux session name for connection.
|
|
286
|
+
tmux_socket: Tmux socket path for connection.
|
|
287
|
+
agent_host: Hostname where the agent is running.
|
|
288
|
+
metadata: Additional metadata to update.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Updated run data or None on failure.
|
|
292
|
+
"""
|
|
293
|
+
client = await self._get_client()
|
|
294
|
+
logger.debug(f"Sending heartbeat for run {run_id}")
|
|
295
|
+
|
|
296
|
+
payload: dict[str, Any] = {}
|
|
297
|
+
if status is not None:
|
|
298
|
+
payload["status"] = status
|
|
299
|
+
if tasks_executed is not None:
|
|
300
|
+
payload["tasks_executed"] = tasks_executed
|
|
301
|
+
if tasks_succeeded is not None:
|
|
302
|
+
payload["tasks_succeeded"] = tasks_succeeded
|
|
303
|
+
if tasks_failed is not None:
|
|
304
|
+
payload["tasks_failed"] = tasks_failed
|
|
305
|
+
if current_task_id is not None:
|
|
306
|
+
payload["current_task_id"] = current_task_id
|
|
307
|
+
if current_task_title is not None:
|
|
308
|
+
payload["current_task_title"] = current_task_title
|
|
309
|
+
if tmux_session is not None:
|
|
310
|
+
payload["tmux_session"] = tmux_session
|
|
311
|
+
if tmux_socket is not None:
|
|
312
|
+
payload["tmux_socket"] = tmux_socket
|
|
313
|
+
if agent_host is not None:
|
|
314
|
+
payload["agent_host"] = agent_host
|
|
315
|
+
if metadata:
|
|
316
|
+
payload["metadata"] = metadata
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
response = await client.post(
|
|
320
|
+
f"{self.api_base}/runs/{run_id}/heartbeat",
|
|
321
|
+
headers=self.headers,
|
|
322
|
+
json=payload if payload else None,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
if response.status_code == 200:
|
|
326
|
+
return RunResponse(**response.json())
|
|
327
|
+
|
|
328
|
+
if response.status_code == 404:
|
|
329
|
+
logger.warning(f"Run {run_id} not found for heartbeat")
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
logger.error(f"Failed to send heartbeat: {response.status_code} - {response.text}")
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
except httpx.RequestError as e:
|
|
336
|
+
logger.error(f"Request error sending heartbeat: {e}")
|
|
337
|
+
return None
|
|
338
|
+
|
|
339
|
+
async def complete_run(
|
|
340
|
+
self,
|
|
341
|
+
run_id: str,
|
|
342
|
+
tasks_executed: int | None = None,
|
|
343
|
+
tasks_succeeded: int | None = None,
|
|
344
|
+
tasks_failed: int | None = None,
|
|
345
|
+
metadata: dict[str, Any] | None = None,
|
|
346
|
+
) -> RunResponse | None:
|
|
347
|
+
"""Mark a run as completed.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
run_id: Run ID to complete.
|
|
351
|
+
tasks_executed: Final tasks executed count.
|
|
352
|
+
tasks_succeeded: Final tasks succeeded count.
|
|
353
|
+
tasks_failed: Final tasks failed count.
|
|
354
|
+
metadata: Additional metadata.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
Updated run data or None on failure.
|
|
358
|
+
"""
|
|
359
|
+
client = await self._get_client()
|
|
360
|
+
logger.info(f"Completing run {run_id}")
|
|
361
|
+
|
|
362
|
+
payload: dict[str, Any] = {}
|
|
363
|
+
if tasks_executed is not None:
|
|
364
|
+
payload["tasks_executed"] = tasks_executed
|
|
365
|
+
if tasks_succeeded is not None:
|
|
366
|
+
payload["tasks_succeeded"] = tasks_succeeded
|
|
367
|
+
if tasks_failed is not None:
|
|
368
|
+
payload["tasks_failed"] = tasks_failed
|
|
369
|
+
if metadata:
|
|
370
|
+
payload["metadata"] = metadata
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
response = await client.patch(
|
|
374
|
+
f"{self.api_base}/runs/{run_id}/complete",
|
|
375
|
+
headers=self.headers,
|
|
376
|
+
json=payload if payload else None,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if response.status_code == 200:
|
|
380
|
+
return RunResponse(**response.json())
|
|
381
|
+
|
|
382
|
+
if response.status_code == 404:
|
|
383
|
+
logger.warning(f"Run {run_id} not found for completion")
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
logger.error(f"Failed to complete run: {response.status_code} - {response.text}")
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
except httpx.RequestError as e:
|
|
390
|
+
logger.error(f"Request error completing run: {e}")
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
async def fail_run(
|
|
394
|
+
self,
|
|
395
|
+
run_id: str,
|
|
396
|
+
error_message: str,
|
|
397
|
+
error_type: str | None = None,
|
|
398
|
+
metadata: dict[str, Any] | None = None,
|
|
399
|
+
) -> RunResponse | None:
|
|
400
|
+
"""Mark a run as failed.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
run_id: Run ID to fail.
|
|
404
|
+
error_message: Error message describing what went wrong.
|
|
405
|
+
error_type: Type/category of the error.
|
|
406
|
+
metadata: Additional metadata.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Updated run data or None on failure.
|
|
410
|
+
"""
|
|
411
|
+
client = await self._get_client()
|
|
412
|
+
logger.info(f"Failing run {run_id}: {error_message}")
|
|
413
|
+
|
|
414
|
+
payload: dict[str, Any] = {"error_message": error_message}
|
|
415
|
+
if error_type:
|
|
416
|
+
payload["error_type"] = error_type
|
|
417
|
+
if metadata:
|
|
418
|
+
payload["metadata"] = metadata
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
response = await client.patch(
|
|
422
|
+
f"{self.api_base}/runs/{run_id}/fail",
|
|
423
|
+
headers=self.headers,
|
|
424
|
+
json=payload,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
if response.status_code == 200:
|
|
428
|
+
return RunResponse(**response.json())
|
|
429
|
+
|
|
430
|
+
if response.status_code == 404:
|
|
431
|
+
logger.warning(f"Run {run_id} not found for failure")
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
logger.error(f"Failed to fail run: {response.status_code} - {response.text}")
|
|
435
|
+
return None
|
|
436
|
+
|
|
437
|
+
except httpx.RequestError as e:
|
|
438
|
+
logger.error(f"Request error failing run: {e}")
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def display_run(run: RunResponse, title: str = "Run") -> None:
|
|
443
|
+
"""Display a run in a formatted panel.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
run: Run data.
|
|
447
|
+
title: Panel title.
|
|
448
|
+
"""
|
|
449
|
+
status_style = STATUS_STYLES.get(run.status, "white")
|
|
450
|
+
|
|
451
|
+
run_info = (
|
|
452
|
+
f"[bold cyan]ID:[/bold cyan] {run.id}\n"
|
|
453
|
+
f"[bold cyan]Session:[/bold cyan] {run.session_name}\n"
|
|
454
|
+
f"[bold cyan]Status:[/bold cyan] [{status_style}]{run.status}[/{status_style}]\n"
|
|
455
|
+
f"[bold cyan]Started:[/bold cyan] {run.started_at}\n"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
if run.completed_at:
|
|
459
|
+
run_info += f"[bold cyan]Completed:[/bold cyan] {run.completed_at}\n"
|
|
460
|
+
|
|
461
|
+
if run.duration_seconds:
|
|
462
|
+
run_info += f"[bold cyan]Duration:[/bold cyan] {run.duration_seconds:.1f}s\n"
|
|
463
|
+
|
|
464
|
+
run_info += (
|
|
465
|
+
f"\n[bold cyan]Tasks:[/bold cyan] "
|
|
466
|
+
f"{run.tasks_succeeded}/{run.tasks_executed} succeeded, "
|
|
467
|
+
f"{run.tasks_failed} failed"
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
if run.agent_id:
|
|
471
|
+
run_info += f"\n[bold cyan]Agent ID:[/bold cyan] {run.agent_id}"
|
|
472
|
+
|
|
473
|
+
if run.application:
|
|
474
|
+
run_info += f"\n[bold cyan]Application:[/bold cyan] {run.application}"
|
|
475
|
+
|
|
476
|
+
if run.error_message:
|
|
477
|
+
run_info += f"\n\n[bold red]Error:[/bold red] {run.error_message}"
|
|
478
|
+
if run.error_type:
|
|
479
|
+
run_info += f" ({run.error_type})"
|
|
480
|
+
|
|
481
|
+
if run.last_heartbeat_at:
|
|
482
|
+
run_info += f"\n[bold cyan]Last Heartbeat:[/bold cyan] {run.last_heartbeat_at}"
|
|
483
|
+
|
|
484
|
+
console.print(Panel(run_info, title=title, border_style="green"))
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def display_run_list(runs: list[RunResponse], full_ids: bool = True) -> None:
|
|
488
|
+
"""Display a list of runs in a formatted table.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
runs: List of run data.
|
|
492
|
+
full_ids: If True, show full UUIDs. If False, truncate.
|
|
493
|
+
"""
|
|
494
|
+
if not runs:
|
|
495
|
+
console.print("[yellow]No runs found[/yellow]")
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
table = Table(title="Runs")
|
|
499
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
500
|
+
table.add_column("Session", style="white")
|
|
501
|
+
table.add_column("Status", style="magenta")
|
|
502
|
+
table.add_column("Tasks", style="yellow")
|
|
503
|
+
table.add_column("Started", style="green")
|
|
504
|
+
table.add_column("Duration", style="blue")
|
|
505
|
+
|
|
506
|
+
for run in runs:
|
|
507
|
+
status_style = STATUS_STYLES.get(run.status, "white")
|
|
508
|
+
|
|
509
|
+
run_id = run.id
|
|
510
|
+
if not full_ids and len(run_id) > 8:
|
|
511
|
+
run_id = run_id[:8] + "..."
|
|
512
|
+
|
|
513
|
+
duration = f"{run.duration_seconds:.1f}s" if run.duration_seconds else "-"
|
|
514
|
+
tasks = f"{run.tasks_succeeded}/{run.tasks_executed}"
|
|
515
|
+
|
|
516
|
+
table.add_row(
|
|
517
|
+
run_id,
|
|
518
|
+
run.session_name[:30],
|
|
519
|
+
f"[{status_style}]{run.status}[/{status_style}]",
|
|
520
|
+
tasks,
|
|
521
|
+
run.started_at[:19] if run.started_at else "-",
|
|
522
|
+
duration,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
console.print(table)
|
|
526
|
+
console.print(f"\n[dim]Total: {len(runs)} runs[/dim]")
|