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,403 @@
|
|
|
1
|
+
"""Sessions management API client for 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
|
+
SessionStatus = Literal["pending", "running", "completed", "failed", "cancelled"]
|
|
17
|
+
|
|
18
|
+
# Status display styles
|
|
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 SessionCreateRequest(BaseModel):
|
|
29
|
+
"""Request model for creating a session."""
|
|
30
|
+
|
|
31
|
+
project_id: str = Field(description="Project ID")
|
|
32
|
+
task_id: str | None = Field(default=None, description="Task being executed")
|
|
33
|
+
agent_id: str | None = Field(default=None, description="Existing agent ID to link to")
|
|
34
|
+
agent_name: str | None = Field(default=None, description="Agent name (creates agent if needed)")
|
|
35
|
+
agent_type: str = Field(description="Agent type (claude, codex, aider)")
|
|
36
|
+
prompt: str = Field(description="Initial prompt sent to agent")
|
|
37
|
+
working_directory: str = Field(description="Working directory for execution")
|
|
38
|
+
metadata: dict[str, Any] | None = Field(default=None, description="Additional metadata")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SessionResponse(BaseModel):
|
|
42
|
+
"""Response model for session data."""
|
|
43
|
+
|
|
44
|
+
id: str
|
|
45
|
+
project_id: str
|
|
46
|
+
task_id: str | None = None
|
|
47
|
+
agent_type: str
|
|
48
|
+
agent_session_id: str | None = None # Native session ID for resume
|
|
49
|
+
status: SessionStatus
|
|
50
|
+
prompt: str
|
|
51
|
+
working_directory: str
|
|
52
|
+
created_at: str
|
|
53
|
+
updated_at: str
|
|
54
|
+
metadata: dict[str, Any] | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SessionListResponse(BaseModel):
|
|
58
|
+
"""Response model for listing sessions."""
|
|
59
|
+
|
|
60
|
+
sessions: list[SessionResponse]
|
|
61
|
+
total: int
|
|
62
|
+
limit: int
|
|
63
|
+
offset: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SessionUpdateRequest(BaseModel):
|
|
67
|
+
"""Request model for updating a session."""
|
|
68
|
+
|
|
69
|
+
status: SessionStatus | None = None
|
|
70
|
+
agent_session_id: str | None = None
|
|
71
|
+
metadata: dict[str, Any] | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SessionsClient:
|
|
75
|
+
"""Async HTTP client for Sessions API.
|
|
76
|
+
|
|
77
|
+
Manages session lifecycle: create, get, update, list.
|
|
78
|
+
Sessions track agent execution and store the native session ID for resume.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, api_key: str | None = None, timeout: float = 30.0) -> None:
|
|
82
|
+
"""Initialize the client.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
api_key: API key for authentication. If not provided, reads from STEERDEV_API_KEY.
|
|
86
|
+
timeout: Request timeout in seconds.
|
|
87
|
+
"""
|
|
88
|
+
self.api_key = api_key or get_api_key()
|
|
89
|
+
self.api_base = get_api_endpoint()
|
|
90
|
+
self.timeout = timeout
|
|
91
|
+
self._client: httpx.AsyncClient | None = None
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def headers(self) -> dict[str, str]:
|
|
95
|
+
"""Get request headers with authentication."""
|
|
96
|
+
return {
|
|
97
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
98
|
+
"Content-Type": "application/json",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
102
|
+
"""Get or create async HTTP client."""
|
|
103
|
+
if self._client is None:
|
|
104
|
+
self._client = httpx.AsyncClient(timeout=self.timeout)
|
|
105
|
+
return self._client
|
|
106
|
+
|
|
107
|
+
async def close(self) -> None:
|
|
108
|
+
"""Close the HTTP client."""
|
|
109
|
+
if self._client is not None:
|
|
110
|
+
await self._client.aclose()
|
|
111
|
+
self._client = None
|
|
112
|
+
|
|
113
|
+
async def __aenter__(self) -> Self:
|
|
114
|
+
"""Enter async context manager."""
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
118
|
+
"""Exit async context manager."""
|
|
119
|
+
await self.close()
|
|
120
|
+
|
|
121
|
+
async def create_session(self, request: SessionCreateRequest) -> SessionResponse | None:
|
|
122
|
+
"""Create a new session.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
request: Session creation request.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Created session data or None on failure.
|
|
129
|
+
"""
|
|
130
|
+
client = await self._get_client()
|
|
131
|
+
logger.debug(f"Creating session at {self.api_base}/sessions")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
response = await client.post(
|
|
135
|
+
f"{self.api_base}/sessions",
|
|
136
|
+
headers=self.headers,
|
|
137
|
+
json=request.model_dump(exclude_none=True),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if response.status_code == 201:
|
|
141
|
+
return SessionResponse(**response.json())
|
|
142
|
+
|
|
143
|
+
logger.error(f"Failed to create session: {response.status_code} - {response.text}")
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
except httpx.RequestError as e:
|
|
147
|
+
logger.error(f"Request error creating session: {e}")
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
async def get_session(self, session_id: str) -> SessionResponse | None:
|
|
151
|
+
"""Get a specific session by ID.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
session_id: Session ID (UUID) to fetch.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Session data or None if not found.
|
|
158
|
+
"""
|
|
159
|
+
client = await self._get_client()
|
|
160
|
+
logger.debug(f"Fetching session {session_id}")
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
response = await client.get(
|
|
164
|
+
f"{self.api_base}/sessions/{session_id}",
|
|
165
|
+
headers=self.headers,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if response.status_code == 200:
|
|
169
|
+
return SessionResponse(**response.json())
|
|
170
|
+
if response.status_code == 404:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
logger.error(f"Failed to get session: {response.status_code} - {response.text}")
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
except httpx.RequestError as e:
|
|
177
|
+
logger.error(f"Request error getting session: {e}")
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
async def update_session(
|
|
181
|
+
self,
|
|
182
|
+
session_id: str,
|
|
183
|
+
status: SessionStatus | None = None,
|
|
184
|
+
agent_session_id: str | None = None,
|
|
185
|
+
metadata: dict[str, Any] | None = None,
|
|
186
|
+
) -> SessionResponse | None:
|
|
187
|
+
"""Update a session.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
session_id: Session ID to update.
|
|
191
|
+
status: New session status.
|
|
192
|
+
agent_session_id: Native agent session ID for resume.
|
|
193
|
+
metadata: Additional metadata to merge.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Updated session data or None on failure.
|
|
197
|
+
"""
|
|
198
|
+
client = await self._get_client()
|
|
199
|
+
logger.debug(f"Updating session {session_id}")
|
|
200
|
+
|
|
201
|
+
payload: dict[str, Any] = {}
|
|
202
|
+
if status is not None:
|
|
203
|
+
payload["status"] = status
|
|
204
|
+
if agent_session_id is not None:
|
|
205
|
+
payload["agent_session_id"] = agent_session_id
|
|
206
|
+
if metadata is not None:
|
|
207
|
+
payload["metadata"] = metadata
|
|
208
|
+
|
|
209
|
+
if not payload:
|
|
210
|
+
logger.warning("No fields to update")
|
|
211
|
+
return await self.get_session(session_id)
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
response = await client.patch(
|
|
215
|
+
f"{self.api_base}/sessions/{session_id}",
|
|
216
|
+
headers=self.headers,
|
|
217
|
+
json=payload,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if response.status_code == 200:
|
|
221
|
+
return SessionResponse(**response.json())
|
|
222
|
+
|
|
223
|
+
if response.status_code == 404:
|
|
224
|
+
logger.warning(f"Session {session_id} not found")
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
logger.error(f"Failed to update session: {response.status_code} - {response.text}")
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
except httpx.RequestError as e:
|
|
231
|
+
logger.error(f"Request error updating session: {e}")
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
async def list_sessions(
|
|
235
|
+
self,
|
|
236
|
+
project_id: str | None = None,
|
|
237
|
+
task_id: str | None = None,
|
|
238
|
+
status: SessionStatus | None = None,
|
|
239
|
+
limit: int = 50,
|
|
240
|
+
offset: int = 0,
|
|
241
|
+
) -> SessionListResponse | None:
|
|
242
|
+
"""List sessions with optional filters.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
project_id: Filter by project ID.
|
|
246
|
+
task_id: Filter by task ID.
|
|
247
|
+
status: Filter by session status.
|
|
248
|
+
limit: Maximum number of sessions to return.
|
|
249
|
+
offset: Offset for pagination.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Session list response or None on failure.
|
|
253
|
+
"""
|
|
254
|
+
client = await self._get_client()
|
|
255
|
+
params: dict[str, str | int] = {"limit": limit, "offset": offset}
|
|
256
|
+
if project_id:
|
|
257
|
+
params["project_id"] = project_id
|
|
258
|
+
if task_id:
|
|
259
|
+
params["task_id"] = task_id
|
|
260
|
+
if status:
|
|
261
|
+
params["status"] = status
|
|
262
|
+
|
|
263
|
+
logger.debug(f"Listing sessions with params: {params}")
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
response = await client.get(
|
|
267
|
+
f"{self.api_base}/sessions",
|
|
268
|
+
headers=self.headers,
|
|
269
|
+
params=params,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if response.status_code == 200:
|
|
273
|
+
return SessionListResponse(**response.json())
|
|
274
|
+
|
|
275
|
+
logger.error(f"Failed to list sessions: {response.status_code} - {response.text}")
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
except httpx.RequestError as e:
|
|
279
|
+
logger.error(f"Request error listing sessions: {e}")
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
async def mark_running(self, session_id: str) -> SessionResponse | None:
|
|
283
|
+
"""Mark a session as running.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
session_id: Session ID to update.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Updated session data or None on failure.
|
|
290
|
+
"""
|
|
291
|
+
return await self.update_session(session_id, status="running")
|
|
292
|
+
|
|
293
|
+
async def mark_completed(
|
|
294
|
+
self,
|
|
295
|
+
session_id: str,
|
|
296
|
+
agent_session_id: str | None = None,
|
|
297
|
+
) -> SessionResponse | None:
|
|
298
|
+
"""Mark a session as completed.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
session_id: Session ID to update.
|
|
302
|
+
agent_session_id: Native agent session ID for resume.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Updated session data or None on failure.
|
|
306
|
+
"""
|
|
307
|
+
return await self.update_session(
|
|
308
|
+
session_id,
|
|
309
|
+
status="completed",
|
|
310
|
+
agent_session_id=agent_session_id,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
async def mark_failed(
|
|
314
|
+
self,
|
|
315
|
+
session_id: str,
|
|
316
|
+
metadata: dict[str, Any] | None = None,
|
|
317
|
+
) -> SessionResponse | None:
|
|
318
|
+
"""Mark a session as failed.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
session_id: Session ID to update.
|
|
322
|
+
metadata: Error details to store.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Updated session data or None on failure.
|
|
326
|
+
"""
|
|
327
|
+
return await self.update_session(session_id, status="failed", metadata=metadata)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def display_session(session: SessionResponse, title: str = "Session") -> None:
|
|
331
|
+
"""Display a session in a formatted panel.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
session: Session data.
|
|
335
|
+
title: Panel title.
|
|
336
|
+
"""
|
|
337
|
+
status_style = STATUS_STYLES.get(session.status, "white")
|
|
338
|
+
|
|
339
|
+
session_info = (
|
|
340
|
+
f"[bold cyan]ID:[/bold cyan] {session.id}\n"
|
|
341
|
+
f"[bold cyan]Project ID:[/bold cyan] {session.project_id}\n"
|
|
342
|
+
f"[bold cyan]Agent Type:[/bold cyan] {session.agent_type}\n"
|
|
343
|
+
f"[bold cyan]Status:[/bold cyan] [{status_style}]{session.status}[/{status_style}]\n"
|
|
344
|
+
f"[bold cyan]Created:[/bold cyan] {session.created_at}\n"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
if session.task_id:
|
|
348
|
+
session_info += f"[bold cyan]Task ID:[/bold cyan] {session.task_id}\n"
|
|
349
|
+
|
|
350
|
+
if session.agent_session_id:
|
|
351
|
+
session_info += f"[bold cyan]Agent Session ID:[/bold cyan] {session.agent_session_id}\n"
|
|
352
|
+
|
|
353
|
+
session_info += f"\n[bold cyan]Working Directory:[/bold cyan] {session.working_directory}"
|
|
354
|
+
|
|
355
|
+
if session.prompt:
|
|
356
|
+
# Truncate long prompts
|
|
357
|
+
prompt_display = (
|
|
358
|
+
session.prompt[:200] + "..." if len(session.prompt) > 200 else session.prompt
|
|
359
|
+
)
|
|
360
|
+
session_info += f"\n\n[bold cyan]Prompt:[/bold cyan]\n{prompt_display}"
|
|
361
|
+
|
|
362
|
+
console.print(Panel(session_info, title=title, border_style="green"))
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def display_session_list(sessions: list[SessionResponse], full_ids: bool = True) -> None:
|
|
366
|
+
"""Display a list of sessions in a formatted table.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
sessions: List of session data.
|
|
370
|
+
full_ids: If True, show full UUIDs. If False, truncate.
|
|
371
|
+
"""
|
|
372
|
+
if not sessions:
|
|
373
|
+
console.print("[yellow]No sessions found[/yellow]")
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
table = Table(title="Sessions")
|
|
377
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
378
|
+
table.add_column("Agent", style="white")
|
|
379
|
+
table.add_column("Status", style="magenta")
|
|
380
|
+
table.add_column("Task ID", style="yellow")
|
|
381
|
+
table.add_column("Created", style="green")
|
|
382
|
+
|
|
383
|
+
for session in sessions:
|
|
384
|
+
status_style = STATUS_STYLES.get(session.status, "white")
|
|
385
|
+
|
|
386
|
+
session_id = session.id
|
|
387
|
+
task_id = session.task_id or "-"
|
|
388
|
+
if not full_ids:
|
|
389
|
+
if len(session_id) > 8:
|
|
390
|
+
session_id = session_id[:8] + "..."
|
|
391
|
+
if task_id != "-" and len(task_id) > 8:
|
|
392
|
+
task_id = task_id[:8] + "..."
|
|
393
|
+
|
|
394
|
+
table.add_row(
|
|
395
|
+
session_id,
|
|
396
|
+
session.agent_type,
|
|
397
|
+
f"[{status_style}]{session.status}[/{status_style}]",
|
|
398
|
+
task_id,
|
|
399
|
+
session.created_at[:19] if session.created_at else "-",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
console.print(table)
|
|
403
|
+
console.print(f"\n[dim]Total: {len(sessions)} sessions[/dim]")
|