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,351 @@
1
+ """Workflow runs API client for managing workflow execution."""
2
+
3
+ from typing import Any, Literal, Self
4
+
5
+ import httpx
6
+ from loguru import logger
7
+ from pydantic import BaseModel, Field
8
+
9
+ from steerdev_agent.api.client import get_api_endpoint, get_api_key
10
+
11
+ WorkflowRunStatus = Literal["pending", "running", "completed", "failed", "cancelled"]
12
+ PhaseRunStatus = Literal["pending", "running", "completed", "failed", "skipped"]
13
+
14
+
15
+ class PhaseRunResponse(BaseModel):
16
+ """Response model for phase run data."""
17
+
18
+ id: str
19
+ workflow_run_id: str
20
+ phase_id: str
21
+ phase_name: str
22
+ phase_type: str
23
+ status: PhaseRunStatus
24
+ attempt_number: int = 1
25
+ input_context: dict[str, Any] = Field(default_factory=dict)
26
+ output_context: dict[str, Any] = Field(default_factory=dict)
27
+ result_summary: str | None = None
28
+ started_at: str | None = None
29
+ completed_at: str | None = None
30
+ error_message: str | None = None
31
+ created_at: str
32
+ updated_at: str
33
+
34
+
35
+ class WorkflowRunResponse(BaseModel):
36
+ """Response model for workflow run data."""
37
+
38
+ id: str
39
+ workflow_id: str
40
+ workflow_name: str
41
+ project_id: str
42
+ run_id: str | None = None
43
+ linear_issue_id: str | None = None
44
+ status: WorkflowRunStatus
45
+ current_phase_id: str | None = None
46
+ current_phase_name: str | None = None
47
+ context: dict[str, Any] = Field(default_factory=dict)
48
+ phases_completed: int = 0
49
+ phases_failed: int = 0
50
+ phases_skipped: int = 0
51
+ total_phases: int = 0
52
+ phase_runs: list[PhaseRunResponse] | None = None
53
+ started_at: str | None = None
54
+ completed_at: str | None = None
55
+ error_message: str | None = None
56
+ created_at: str
57
+ updated_at: str
58
+
59
+
60
+ class WorkflowRunsClient:
61
+ """Async HTTP client for workflow run lifecycle management.
62
+
63
+ Manages the full workflow run lifecycle: start, advance phases,
64
+ complete/fail phases, and track progress.
65
+ """
66
+
67
+ def __init__(self, api_key: str | None = None, timeout: float = 30.0) -> None:
68
+ """Initialize the client.
69
+
70
+ Args:
71
+ api_key: API key for authentication. If not provided, reads from STEERDEV_API_KEY.
72
+ timeout: Request timeout in seconds.
73
+ """
74
+ self.api_key = api_key or get_api_key()
75
+ self.api_base = get_api_endpoint()
76
+ self.timeout = timeout
77
+ self._client: httpx.AsyncClient | None = None
78
+
79
+ @property
80
+ def headers(self) -> dict[str, str]:
81
+ """Get request headers with authentication."""
82
+ return {
83
+ "Authorization": f"Bearer {self.api_key}",
84
+ "Content-Type": "application/json",
85
+ }
86
+
87
+ async def _get_client(self) -> httpx.AsyncClient:
88
+ """Get or create async HTTP client."""
89
+ if self._client is None:
90
+ self._client = httpx.AsyncClient(timeout=self.timeout)
91
+ return self._client
92
+
93
+ async def close(self) -> None:
94
+ """Close the HTTP client."""
95
+ if self._client is not None:
96
+ await self._client.aclose()
97
+ self._client = None
98
+
99
+ async def __aenter__(self) -> Self:
100
+ """Enter async context manager."""
101
+ return self
102
+
103
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
104
+ """Exit async context manager."""
105
+ await self.close()
106
+
107
+ async def start_workflow(
108
+ self,
109
+ workflow_id: str,
110
+ run_id: str | None = None,
111
+ linear_issue_id: str | None = None,
112
+ initial_context: dict[str, Any] | None = None,
113
+ ) -> WorkflowRunResponse | None:
114
+ """Start a new workflow run.
115
+
116
+ Args:
117
+ workflow_id: ID of the workflow to execute.
118
+ run_id: Optional associated agent run ID.
119
+ linear_issue_id: Optional Linear issue being processed.
120
+ initial_context: Initial context data for the workflow.
121
+
122
+ Returns:
123
+ Created workflow run or None on failure.
124
+ """
125
+ client = await self._get_client()
126
+ logger.debug(f"Starting workflow {workflow_id}")
127
+
128
+ payload: dict[str, Any] = {"workflow_id": workflow_id}
129
+ if run_id:
130
+ payload["run_id"] = run_id
131
+ if linear_issue_id:
132
+ payload["linear_issue_id"] = linear_issue_id
133
+ if initial_context:
134
+ payload["initial_context"] = initial_context
135
+
136
+ try:
137
+ response = await client.post(
138
+ f"{self.api_base}/workflow-runs",
139
+ headers=self.headers,
140
+ json=payload,
141
+ )
142
+
143
+ if response.status_code == 201:
144
+ return WorkflowRunResponse(**response.json())
145
+
146
+ logger.error(f"Failed to start workflow: {response.status_code} - {response.text}")
147
+ return None
148
+
149
+ except httpx.RequestError as e:
150
+ logger.error(f"Request error starting workflow: {e}")
151
+ return None
152
+
153
+ async def get_workflow_run(
154
+ self,
155
+ workflow_run_id: str,
156
+ ) -> WorkflowRunResponse | None:
157
+ """Get a specific workflow run by ID.
158
+
159
+ Args:
160
+ workflow_run_id: Workflow run ID to fetch.
161
+
162
+ Returns:
163
+ Workflow run data or None if not found.
164
+ """
165
+ client = await self._get_client()
166
+ logger.debug(f"Fetching workflow run {workflow_run_id}")
167
+
168
+ try:
169
+ response = await client.get(
170
+ f"{self.api_base}/workflow-runs/{workflow_run_id}",
171
+ headers=self.headers,
172
+ )
173
+
174
+ if response.status_code == 200:
175
+ return WorkflowRunResponse(**response.json())
176
+ if response.status_code == 404:
177
+ return None
178
+
179
+ logger.error(f"Failed to get workflow run: {response.status_code} - {response.text}")
180
+ return None
181
+
182
+ except httpx.RequestError as e:
183
+ logger.error(f"Request error getting workflow run: {e}")
184
+ return None
185
+
186
+ async def advance_phase(
187
+ self,
188
+ workflow_run_id: str,
189
+ output_context: dict[str, Any] | None = None,
190
+ result_summary: str | None = None,
191
+ ) -> WorkflowRunResponse | None:
192
+ """Advance to the next phase after completing the current one.
193
+
194
+ Args:
195
+ workflow_run_id: Workflow run ID to advance.
196
+ output_context: Context data produced by the current phase.
197
+ result_summary: Human-readable summary of the phase result.
198
+
199
+ Returns:
200
+ Updated workflow run or None on failure.
201
+ """
202
+ client = await self._get_client()
203
+ logger.debug(f"Advancing workflow run {workflow_run_id}")
204
+
205
+ payload: dict[str, Any] = {"action": "complete"}
206
+ if output_context:
207
+ payload["output_context"] = output_context
208
+ if result_summary:
209
+ payload["result_summary"] = result_summary
210
+
211
+ try:
212
+ response = await client.post(
213
+ f"{self.api_base}/workflow-runs/{workflow_run_id}/advance",
214
+ headers=self.headers,
215
+ json=payload,
216
+ )
217
+
218
+ if response.status_code == 200:
219
+ return WorkflowRunResponse(**response.json())
220
+
221
+ logger.error(f"Failed to advance phase: {response.status_code} - {response.text}")
222
+ return None
223
+
224
+ except httpx.RequestError as e:
225
+ logger.error(f"Request error advancing phase: {e}")
226
+ return None
227
+
228
+ async def fail_phase(
229
+ self,
230
+ workflow_run_id: str,
231
+ error_message: str,
232
+ ) -> WorkflowRunResponse | None:
233
+ """Mark the current phase as failed.
234
+
235
+ The workflow engine will handle retries or skip logic based on
236
+ the phase configuration.
237
+
238
+ Args:
239
+ workflow_run_id: Workflow run ID.
240
+ error_message: Error message describing the failure.
241
+
242
+ Returns:
243
+ Updated workflow run or None on failure.
244
+ """
245
+ client = await self._get_client()
246
+ logger.debug(f"Failing phase for workflow run {workflow_run_id}")
247
+
248
+ payload: dict[str, Any] = {
249
+ "action": "fail",
250
+ "error_message": error_message,
251
+ }
252
+
253
+ try:
254
+ response = await client.post(
255
+ f"{self.api_base}/workflow-runs/{workflow_run_id}/advance",
256
+ headers=self.headers,
257
+ json=payload,
258
+ )
259
+
260
+ if response.status_code == 200:
261
+ return WorkflowRunResponse(**response.json())
262
+
263
+ logger.error(f"Failed to fail phase: {response.status_code} - {response.text}")
264
+ return None
265
+
266
+ except httpx.RequestError as e:
267
+ logger.error(f"Request error failing phase: {e}")
268
+ return None
269
+
270
+ async def update_phase_run(
271
+ self,
272
+ workflow_run_id: str,
273
+ phase_id: str,
274
+ status: PhaseRunStatus | None = None,
275
+ output_context: dict[str, Any] | None = None,
276
+ result_summary: str | None = None,
277
+ error_message: str | None = None,
278
+ ) -> PhaseRunResponse | None:
279
+ """Update a phase run's status or context.
280
+
281
+ Args:
282
+ workflow_run_id: Workflow run ID.
283
+ phase_id: Phase ID to update.
284
+ status: New phase run status.
285
+ output_context: Updated output context.
286
+ result_summary: Updated result summary.
287
+ error_message: Error message if status is failed.
288
+
289
+ Returns:
290
+ Updated phase run or None on failure.
291
+ """
292
+ client = await self._get_client()
293
+ logger.debug(f"Updating phase run {phase_id} for workflow run {workflow_run_id}")
294
+
295
+ payload: dict[str, Any] = {}
296
+ if status is not None:
297
+ payload["status"] = status
298
+ if output_context is not None:
299
+ payload["output_context"] = output_context
300
+ if result_summary is not None:
301
+ payload["result_summary"] = result_summary
302
+ if error_message is not None:
303
+ payload["error_message"] = error_message
304
+
305
+ try:
306
+ response = await client.patch(
307
+ f"{self.api_base}/workflow-runs/{workflow_run_id}/phases/{phase_id}",
308
+ headers=self.headers,
309
+ json=payload,
310
+ )
311
+
312
+ if response.status_code == 200:
313
+ return PhaseRunResponse(**response.json())
314
+
315
+ logger.error(f"Failed to update phase run: {response.status_code} - {response.text}")
316
+ return None
317
+
318
+ except httpx.RequestError as e:
319
+ logger.error(f"Request error updating phase run: {e}")
320
+ return None
321
+
322
+ async def cancel_workflow_run(
323
+ self,
324
+ workflow_run_id: str,
325
+ ) -> WorkflowRunResponse | None:
326
+ """Cancel a workflow run.
327
+
328
+ Args:
329
+ workflow_run_id: Workflow run ID to cancel.
330
+
331
+ Returns:
332
+ Updated workflow run or None on failure.
333
+ """
334
+ client = await self._get_client()
335
+ logger.debug(f"Cancelling workflow run {workflow_run_id}")
336
+
337
+ try:
338
+ response = await client.delete(
339
+ f"{self.api_base}/workflow-runs/{workflow_run_id}",
340
+ headers=self.headers,
341
+ )
342
+
343
+ if response.status_code == 200:
344
+ return WorkflowRunResponse(**response.json())
345
+
346
+ logger.error(f"Failed to cancel workflow run: {response.status_code} - {response.text}")
347
+ return None
348
+
349
+ except httpx.RequestError as e:
350
+ logger.error(f"Request error cancelling workflow run: {e}")
351
+ return None
@@ -0,0 +1,191 @@
1
+ """Workflows API client for fetching workflow definitions."""
2
+
3
+ from typing import Any, Self
4
+
5
+ import httpx
6
+ from loguru import logger
7
+ from pydantic import BaseModel, Field
8
+
9
+ from steerdev_agent.api.client import get_api_endpoint, get_api_key
10
+
11
+
12
+ class WorkflowPhaseResponse(BaseModel):
13
+ """Response model for workflow phase data."""
14
+
15
+ id: str
16
+ workflow_id: str
17
+ name: str
18
+ phase_type: str
19
+ phase_order: int
20
+ prompt_template: str | None = None
21
+ is_required: bool = True
22
+ max_retries: int = 0
23
+ config_ids: list[str] = Field(default_factory=list)
24
+ created_at: str
25
+ updated_at: str
26
+
27
+
28
+ class WorkflowResponse(BaseModel):
29
+ """Response model for workflow data."""
30
+
31
+ id: str
32
+ project_id: str
33
+ name: str
34
+ description: str | None = None
35
+ is_default: bool = False
36
+ max_retries_per_phase: int = 3
37
+ phases: list[WorkflowPhaseResponse] = Field(default_factory=list)
38
+ created_at: str
39
+ updated_at: str
40
+
41
+
42
+ class WorkflowsClient:
43
+ """Async HTTP client for fetching workflow definitions.
44
+
45
+ Workflows define multi-phase task execution patterns.
46
+ """
47
+
48
+ def __init__(self, api_key: str | None = None, timeout: float = 30.0) -> None:
49
+ """Initialize the client.
50
+
51
+ Args:
52
+ api_key: API key for authentication. If not provided, reads from STEERDEV_API_KEY.
53
+ timeout: Request timeout in seconds.
54
+ """
55
+ self.api_key = api_key or get_api_key()
56
+ self.api_base = get_api_endpoint()
57
+ self.timeout = timeout
58
+ self._client: httpx.AsyncClient | None = None
59
+
60
+ @property
61
+ def headers(self) -> dict[str, str]:
62
+ """Get request headers with authentication."""
63
+ return {
64
+ "Authorization": f"Bearer {self.api_key}",
65
+ "Content-Type": "application/json",
66
+ }
67
+
68
+ async def _get_client(self) -> httpx.AsyncClient:
69
+ """Get or create async HTTP client."""
70
+ if self._client is None:
71
+ self._client = httpx.AsyncClient(timeout=self.timeout)
72
+ return self._client
73
+
74
+ async def close(self) -> None:
75
+ """Close the HTTP client."""
76
+ if self._client is not None:
77
+ await self._client.aclose()
78
+ self._client = None
79
+
80
+ async def __aenter__(self) -> Self:
81
+ """Enter async context manager."""
82
+ return self
83
+
84
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
85
+ """Exit async context manager."""
86
+ await self.close()
87
+
88
+ async def get_workflow(self, workflow_id: str) -> WorkflowResponse | None:
89
+ """Get a workflow by ID.
90
+
91
+ Args:
92
+ workflow_id: Workflow ID to fetch.
93
+
94
+ Returns:
95
+ Workflow data or None if not found.
96
+ """
97
+ client = await self._get_client()
98
+ logger.debug(f"Fetching workflow {workflow_id}")
99
+
100
+ try:
101
+ response = await client.get(
102
+ f"{self.api_base}/workflows/{workflow_id}",
103
+ headers=self.headers,
104
+ )
105
+
106
+ if response.status_code == 200:
107
+ return WorkflowResponse(**response.json())
108
+ if response.status_code == 404:
109
+ logger.warning(f"Workflow not found: {workflow_id}")
110
+ return None
111
+
112
+ logger.error(f"Failed to get workflow: {response.status_code} - {response.text}")
113
+ return None
114
+
115
+ except httpx.RequestError as e:
116
+ logger.error(f"Request error getting workflow: {e}")
117
+ return None
118
+
119
+ async def get_default_workflow(self, project_id: str) -> WorkflowResponse | None:
120
+ """Get the default workflow for a project.
121
+
122
+ Args:
123
+ project_id: Project ID to get default workflow for.
124
+
125
+ Returns:
126
+ Default workflow or None if not found.
127
+ """
128
+ client = await self._get_client()
129
+ logger.debug(f"Fetching default workflow for project {project_id}")
130
+
131
+ try:
132
+ response = await client.get(
133
+ f"{self.api_base}/workflows",
134
+ headers=self.headers,
135
+ params={"project_id": project_id, "is_default": "true"},
136
+ )
137
+
138
+ if response.status_code == 200:
139
+ data = response.json()
140
+ workflows = data.get("workflows", [])
141
+ if workflows:
142
+ return WorkflowResponse(**workflows[0])
143
+ return None
144
+
145
+ logger.error(
146
+ f"Failed to get default workflow: {response.status_code} - {response.text}"
147
+ )
148
+ return None
149
+
150
+ except httpx.RequestError as e:
151
+ logger.error(f"Request error getting default workflow: {e}")
152
+ return None
153
+
154
+ async def list_workflows(
155
+ self,
156
+ project_id: str | None = None,
157
+ limit: int = 50,
158
+ ) -> list[WorkflowResponse]:
159
+ """List workflows with optional filters.
160
+
161
+ Args:
162
+ project_id: Filter by project ID.
163
+ limit: Maximum number of workflows to return.
164
+
165
+ Returns:
166
+ List of workflows.
167
+ """
168
+ client = await self._get_client()
169
+ logger.debug("Listing workflows")
170
+
171
+ params: dict[str, Any] = {"limit": limit}
172
+ if project_id:
173
+ params["project_id"] = project_id
174
+
175
+ try:
176
+ response = await client.get(
177
+ f"{self.api_base}/workflows",
178
+ headers=self.headers,
179
+ params=params,
180
+ )
181
+
182
+ if response.status_code == 200:
183
+ data = response.json()
184
+ return [WorkflowResponse(**w) for w in data.get("workflows", [])]
185
+
186
+ logger.error(f"Failed to list workflows: {response.status_code} - {response.text}")
187
+ return []
188
+
189
+ except httpx.RequestError as e:
190
+ logger.error(f"Request error listing workflows: {e}")
191
+ return []