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.
Files changed (57) hide show
  1. steerdev-0.4.27.dist-info/METADATA +224 -0
  2. steerdev-0.4.27.dist-info/RECORD +57 -0
  3. steerdev-0.4.27.dist-info/WHEEL +4 -0
  4. steerdev-0.4.27.dist-info/entry_points.txt +2 -0
  5. steerdev_agent/__init__.py +10 -0
  6. steerdev_agent/api/__init__.py +32 -0
  7. steerdev_agent/api/activity.py +278 -0
  8. steerdev_agent/api/agents.py +145 -0
  9. steerdev_agent/api/client.py +158 -0
  10. steerdev_agent/api/commands.py +399 -0
  11. steerdev_agent/api/configs.py +238 -0
  12. steerdev_agent/api/context.py +306 -0
  13. steerdev_agent/api/events.py +294 -0
  14. steerdev_agent/api/hooks.py +178 -0
  15. steerdev_agent/api/implementation_plan.py +408 -0
  16. steerdev_agent/api/messages.py +231 -0
  17. steerdev_agent/api/prd.py +281 -0
  18. steerdev_agent/api/runs.py +526 -0
  19. steerdev_agent/api/sessions.py +403 -0
  20. steerdev_agent/api/specs.py +321 -0
  21. steerdev_agent/api/tasks.py +659 -0
  22. steerdev_agent/api/workflow_runs.py +351 -0
  23. steerdev_agent/api/workflows.py +191 -0
  24. steerdev_agent/cli.py +2254 -0
  25. steerdev_agent/config/__init__.py +19 -0
  26. steerdev_agent/config/models.py +236 -0
  27. steerdev_agent/config/platform.py +272 -0
  28. steerdev_agent/config/settings.py +62 -0
  29. steerdev_agent/daemon.py +675 -0
  30. steerdev_agent/executor/__init__.py +64 -0
  31. steerdev_agent/executor/base.py +121 -0
  32. steerdev_agent/executor/claude.py +328 -0
  33. steerdev_agent/executor/stream.py +163 -0
  34. steerdev_agent/git/__init__.py +1 -0
  35. steerdev_agent/handlers/__init__.py +5 -0
  36. steerdev_agent/handlers/prd.py +533 -0
  37. steerdev_agent/integration.py +334 -0
  38. steerdev_agent/prompt/__init__.py +10 -0
  39. steerdev_agent/prompt/builder.py +263 -0
  40. steerdev_agent/prompt/templates.py +422 -0
  41. steerdev_agent/py.typed +0 -0
  42. steerdev_agent/runner.py +829 -0
  43. steerdev_agent/setup/__init__.py +5 -0
  44. steerdev_agent/setup/claude_setup.py +560 -0
  45. steerdev_agent/setup/templates/claude_md_section.md +140 -0
  46. steerdev_agent/setup/templates/settings.json +69 -0
  47. steerdev_agent/setup/templates/skills/activity/SKILL.md +160 -0
  48. steerdev_agent/setup/templates/skills/context/SKILL.md +122 -0
  49. steerdev_agent/setup/templates/skills/git-workflow/SKILL.md +218 -0
  50. steerdev_agent/setup/templates/skills/progress-logging/SKILL.md +211 -0
  51. steerdev_agent/setup/templates/skills/specs-management/SKILL.md +161 -0
  52. steerdev_agent/setup/templates/skills/task-management/SKILL.md +343 -0
  53. steerdev_agent/setup/templates/steerdev.yaml +51 -0
  54. steerdev_agent/version.py +149 -0
  55. steerdev_agent/workflow/__init__.py +10 -0
  56. steerdev_agent/workflow/executor.py +494 -0
  57. 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]")