devops-bot-sdk 1.2.1__tar.gz → 1.4.0__tar.gz

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 (33) hide show
  1. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
  4. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/pyproject.toml +1 -1
  5. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/__init__.py +2 -2
  6. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/client.py +291 -17
  7. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/config.py +3 -0
  8. devops_bot_sdk-1.4.0/sdk/ipc/handlers.py +816 -0
  9. devops_bot_sdk-1.4.0/sdk/local_exec.py +175 -0
  10. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/models/requests.py +33 -5
  11. devops_bot_sdk-1.4.0/sdk/models/responses.py +165 -0
  12. devops_bot_sdk-1.4.0/sdk/test_pipeline.py +391 -0
  13. devops_bot_sdk-1.2.1/sdk/ipc/handlers.py +0 -266
  14. devops_bot_sdk-1.2.1/sdk/models/responses.py +0 -67
  15. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/README.md +0 -0
  16. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  17. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  18. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/devops_bot_sdk.egg-info/requires.txt +0 -0
  19. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  20. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/collectors/__init__.py +0 -0
  21. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/collectors/files.py +0 -0
  22. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/collectors/process.py +0 -0
  23. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/collectors/screenshot.py +0 -0
  24. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/exceptions.py +0 -0
  25. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/ipc/__init__.py +0 -0
  26. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/ipc/electron_bridge.py +0 -0
  27. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/models/__init__.py +0 -0
  28. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/models/envelope.py +0 -0
  29. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/models/snapshots.py +0 -0
  30. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/py.typed +0 -0
  31. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/sse.py +0 -0
  32. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/sdk/test.py +0 -0
  33. {devops_bot_sdk-1.2.1 → devops_bot_sdk-1.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.2.1
3
+ Version: 1.4.0
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.2.1
3
+ Version: 1.4.0
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -10,9 +10,11 @@ sdk/__init__.py
10
10
  sdk/client.py
11
11
  sdk/config.py
12
12
  sdk/exceptions.py
13
+ sdk/local_exec.py
13
14
  sdk/py.typed
14
15
  sdk/sse.py
15
16
  sdk/test.py
17
+ sdk/test_pipeline.py
16
18
  sdk/collectors/__init__.py
17
19
  sdk/collectors/files.py
18
20
  sdk/collectors/process.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devops-bot-sdk"
7
- version = "1.2.1"
7
+ version = "1.4.0"
8
8
  description = "DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.1.0
3
+ Version: 1.4.0
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -30,7 +30,7 @@ Rules:
30
30
  - All data egress through submit_webhook only
31
31
  """
32
32
 
33
- __version__ = "1.1.0"
33
+ __version__ = "1.4.0"
34
34
  __author__ = "AgentOS"
35
35
 
36
36
  from sdk.client import BackendClient
@@ -29,15 +29,18 @@ from sdk.models.requests import (
29
29
  from sdk.models.responses import (
30
30
  BootstrapData, ChatHistory, OAuthStarted, PipelineResult, PipelineStatus,
31
31
  ThreadSummary, WebhookAck,
32
+ OrchestrateAutoStatusResponse, OrchestrateAutoStopResponse, OrchestrateAutoResumeResponse,
33
+ OrchestrateAutoStopAllResponse, OrchestrateAutoResumeAllResponse,
32
34
  )
33
35
  from sdk.sse import _check_status, stream_with_reconnect
34
36
 
35
37
  logger = logging.getLogger(__name__)
36
38
 
37
- SDK_VERSION = "1.1.0"
39
+ SDK_VERSION = "1.4.0"
38
40
  _POLL_INTERVAL = 3.0
39
41
  _POLL_TIMEOUT = 600.0
40
- _ORCHESTRATE_TIMEOUT = 900.0 # 15 min — a real Claude Code dev run can take minutes
42
+ _ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
43
+ _AUTO_ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — same budget for multi-task auto runs
41
44
 
42
45
 
43
46
  class BackendClient:
@@ -248,38 +251,273 @@ class BackendClient:
248
251
  _check_status(resp.status_code, self._base_url)
249
252
  return resp.json()
250
253
 
251
- async def orchestrate(self, req: OrchestrateRequest) -> PipelineResult:
252
- """POST /api/v1/orchestrate — run the full System-B pipeline synchronously.
253
-
254
- This is the path that honours `dev_engine` / `model` / `apply_diffs`
255
- and drives the ML_TASK_FLOW lifecycle (`jira_task_id` → In Progress /
256
- approval gate / Done). Unlike `orchestrator_run` (async 202 + poll on
257
- /orchestrator/run), this blocks until the run finishes — a real Claude
258
- Code dev run can take minutes, hence the long timeout.
259
-
260
- `user_id` is auto-filled from the token when omitted.
254
+ async def orchestrate(
255
+ self,
256
+ req: OrchestrateRequest,
257
+ on_status_update=None,
258
+ ) -> PipelineResult:
259
+ """POST /api/v1/orchestrate (background) poll status PipelineResult.
260
+
261
+ Runs the full System-B pipeline (engine selection + ML_TASK_FLOW lifecycle).
262
+ A real dev run takes minutes, so this uses the backend's **background**
263
+ mode: it POSTs `background:true` (202 + session_id), then polls
264
+ /orchestrate/status/{session_id} until the run is terminal — avoiding
265
+ HTTP timeouts on the long call.
266
+
267
+ Minimal request: OrchestrateRequest(jira_task_id="..."). The backend
268
+ hydrates user_id from the co_ token, role_slug from the profile,
269
+ user_input from the task's summary + description, and project_path via
270
+ its AI workspace resolver — so none of them need to be sent.
271
+
272
+ `on_status_update`: optional async callable(status: str, state: dict).
273
+ Called whenever the polled status changes — useful for emitting
274
+ intermediate `awaiting_approval` events to the IPC layer without
275
+ stopping the poll loop.
261
276
  """
262
277
  body = req.model_dump(exclude_none=True)
278
+ body["background"] = True
279
+ # Best-effort local fill — the backend derives user_id from the co_
280
+ # token anyway, so a token in an unexpected shape is not an error.
263
281
  if not body.get("user_id"):
264
282
  derived = self._user_id_from_token()
265
- if not derived:
266
- raise BackendAuthFailed(self._url("/api/v1/orchestrate"))
267
- body["user_id"] = derived
283
+ if derived:
284
+ body["user_id"] = derived
268
285
 
286
+ # 1. Kick off the background run (202).
269
287
  try:
270
- async with httpx.AsyncClient(timeout=_ORCHESTRATE_TIMEOUT) as client:
288
+ async with httpx.AsyncClient(timeout=30.0) as client:
271
289
  resp = await client.post(
272
290
  self._url("/api/v1/orchestrate"),
273
291
  json=body,
274
292
  headers=self._headers,
275
293
  )
276
294
  _check_status(resp.status_code, self._url("/api/v1/orchestrate"))
277
- return PipelineResult(**resp.json())
295
+ session_id = resp.json()["session_id"]
296
+ except (BackendAuthFailed, BackendVersionTooOld):
297
+ raise
298
+ except Exception as exc:
299
+ raise BackendUnreachable(self._base_url, str(exc)) from exc
300
+
301
+ # 2. Poll until terminal (or the overall deadline).
302
+ # "awaiting_approval" is NOT terminal — we keep polling so the client
303
+ # receives the final result after the approver replies on WhatsApp.
304
+ terminal = {"completed", "failed"}
305
+ last_reported_status: str | None = None
306
+ last_reported_step: str | None = None
307
+ deadline = asyncio.get_event_loop().time() + _ORCHESTRATE_TIMEOUT
308
+ while asyncio.get_event_loop().time() < deadline:
309
+ await asyncio.sleep(_POLL_INTERVAL)
310
+ try:
311
+ state = await self._orchestrate_status(session_id)
312
+ except (BackendAuthFailed, BackendVersionTooOld):
313
+ raise
314
+ except Exception:
315
+ continue # transient — keep polling
316
+
317
+ current_status = str(state.get("status", ""))
318
+ current_step = state.get("current_step")
319
+
320
+ # Notify caller on any status or step change — lets the IPC layer
321
+ # forward real-time progress events to the desktop without stopping
322
+ # the poll loop.
323
+ if on_status_update and (
324
+ current_status != last_reported_status
325
+ or current_step != last_reported_step
326
+ ):
327
+ last_reported_status = current_status
328
+ last_reported_step = current_step
329
+ try:
330
+ await on_status_update(current_status, state)
331
+ except Exception:
332
+ pass
333
+
334
+ if current_status in terminal:
335
+ return PipelineResult(**state)
336
+ return PipelineResult(session_id=session_id, status="failed",
337
+ error="Pipeline poll timed out.")
338
+
339
+ async def _orchestrate_status(self, session_id: str) -> dict:
340
+ """GET /api/v1/orchestrate/status/{session_id}"""
341
+ async with httpx.AsyncClient(timeout=15.0) as client:
342
+ resp = await client.get(
343
+ self._url(f"/api/v1/orchestrate/status/{session_id}"),
344
+ headers=self._headers,
345
+ )
346
+ _check_status(resp.status_code, self._base_url)
347
+ return resp.json()
348
+
349
+ # ── orchestrate-auto ──────────────────────────────────────────────────
350
+
351
+ async def orchestrate_auto(
352
+ self,
353
+ on_status_update=None,
354
+ ) -> OrchestrateAutoStatusResponse:
355
+ """POST /api/v1/orchestrate-auto → poll status → OrchestrateAutoStatusResponse.
356
+
357
+ Auto-discovers all "To Do" Jira tasks assigned to the current user and
358
+ runs the full System-B pipeline for each sequentially in the background.
359
+ Returns 202 immediately; this method polls GET /api/v1/orchestrate-auto/status
360
+ until the overall run reaches a terminal state (completed / failed / no_tasks).
361
+
362
+ on_status_update: optional async callable(auto_status: str, state: dict).
363
+ Called whenever auto_status or any per-task status changes.
364
+ """
365
+ try:
366
+ async with httpx.AsyncClient(timeout=30.0) as client:
367
+ resp = await client.post(
368
+ self._url("/api/v1/orchestrate-auto"),
369
+ headers=self._headers,
370
+ )
371
+ _check_status(resp.status_code, self._url("/api/v1/orchestrate-auto"))
372
+ except (BackendAuthFailed, BackendVersionTooOld):
373
+ raise
374
+ except Exception as exc:
375
+ raise BackendUnreachable(self._base_url, str(exc)) from exc
376
+
377
+ return await self._poll_auto_terminal(on_status_update)
378
+
379
+ async def _poll_auto_terminal(
380
+ self,
381
+ on_status_update=None,
382
+ ) -> OrchestrateAutoStatusResponse:
383
+ """Poll GET /orchestrate-auto/status until the run reaches a terminal state.
384
+
385
+ `paused` counts as terminal: a whole-run stop (this client's or another's)
386
+ ends the wait — the run continues only after an explicit resume.
387
+ """
388
+ terminal = {"completed", "failed", "no_tasks", "paused"}
389
+ last_auto_status: str | None = None
390
+ last_task_statuses: dict[str, str] = {}
391
+ deadline = asyncio.get_event_loop().time() + _AUTO_ORCHESTRATE_TIMEOUT
392
+
393
+ while asyncio.get_event_loop().time() < deadline:
394
+ await asyncio.sleep(_POLL_INTERVAL)
395
+ try:
396
+ state = await self._orchestrate_auto_status()
397
+ except (BackendAuthFailed, BackendVersionTooOld):
398
+ raise
399
+ except Exception:
400
+ continue
401
+
402
+ auto_status = str(state.get("auto_status", ""))
403
+ tasks = state.get("tasks", [])
404
+ current_task_statuses = {
405
+ t.get("session_id", ""): t.get("status", "") for t in tasks
406
+ }
407
+
408
+ if on_status_update and (
409
+ auto_status != last_auto_status
410
+ or current_task_statuses != last_task_statuses
411
+ ):
412
+ last_auto_status = auto_status
413
+ last_task_statuses = current_task_statuses
414
+ try:
415
+ await on_status_update(auto_status, state)
416
+ except Exception:
417
+ pass
418
+
419
+ if auto_status in terminal:
420
+ return OrchestrateAutoStatusResponse(**state)
421
+
422
+ return OrchestrateAutoStatusResponse(auto_status="failed")
423
+
424
+ async def orchestrate_auto_stop(self, session_id: str) -> OrchestrateAutoStopResponse:
425
+ """POST /api/v1/orchestrate-auto/stop/{session_id} — pause a running pipeline.
426
+
427
+ Cancels the asyncio background task on the server and saves a resume
428
+ snapshot to Redis. The session can be continued via orchestrate_auto_resume().
429
+ """
430
+ url = self._url(f"/api/v1/orchestrate-auto/stop/{session_id}")
431
+ try:
432
+ async with httpx.AsyncClient(timeout=30.0) as client:
433
+ resp = await client.post(url, headers=self._headers)
434
+ _check_status(resp.status_code, url)
435
+ return OrchestrateAutoStopResponse(**resp.json())
278
436
  except (BackendAuthFailed, BackendVersionTooOld):
279
437
  raise
280
438
  except Exception as exc:
281
439
  raise BackendUnreachable(self._base_url, str(exc)) from exc
282
440
 
441
+ async def orchestrate_auto_resume(
442
+ self,
443
+ session_id: str,
444
+ on_status_update=None,
445
+ ) -> OrchestrateAutoStatusResponse:
446
+ """POST /api/v1/orchestrate-auto/resume/{session_id} → poll status → OrchestrateAutoStatusResponse.
447
+
448
+ Resumes a paused pipeline session from its last LangGraph checkpoint —
449
+ no previously-completed steps are re-executed. Returns 202 immediately;
450
+ this method polls GET /api/v1/orchestrate-auto/status until the run is terminal.
451
+
452
+ on_status_update: optional async callable(auto_status: str, state: dict).
453
+ """
454
+ url = self._url(f"/api/v1/orchestrate-auto/resume/{session_id}")
455
+ try:
456
+ async with httpx.AsyncClient(timeout=30.0) as client:
457
+ resp = await client.post(url, headers=self._headers)
458
+ _check_status(resp.status_code, url)
459
+ except (BackendAuthFailed, BackendVersionTooOld):
460
+ raise
461
+ except Exception as exc:
462
+ raise BackendUnreachable(self._base_url, str(exc)) from exc
463
+
464
+ return await self._poll_auto_terminal(on_status_update)
465
+
466
+ async def orchestrate_auto_stop_all(self) -> OrchestrateAutoStopAllResponse:
467
+ """POST /api/v1/orchestrate-auto/stop — pause the WHOLE auto run.
468
+
469
+ The in-flight session is cancelled and checkpointed (paused), every
470
+ session still queued stays queued, and auto_status becomes `paused`.
471
+ Continue the run with orchestrate_auto_resume_all(). For pausing a
472
+ single session only, use orchestrate_auto_stop(session_id).
473
+ """
474
+ url = self._url("/api/v1/orchestrate-auto/stop")
475
+ try:
476
+ async with httpx.AsyncClient(timeout=30.0) as client:
477
+ resp = await client.post(url, headers=self._headers)
478
+ _check_status(resp.status_code, url)
479
+ return OrchestrateAutoStopAllResponse(**resp.json())
480
+ except (BackendAuthFailed, BackendVersionTooOld):
481
+ raise
482
+ except Exception as exc:
483
+ raise BackendUnreachable(self._base_url, str(exc)) from exc
484
+
485
+ async def orchestrate_auto_resume_all(
486
+ self,
487
+ on_status_update=None,
488
+ ) -> OrchestrateAutoStatusResponse:
489
+ """POST /api/v1/orchestrate-auto/resume → poll status → OrchestrateAutoStatusResponse.
490
+
491
+ Resumes the WHOLE auto run paused via orchestrate_auto_stop_all(): the
492
+ paused session continues from its LangGraph checkpoint (no steps
493
+ re-executed), then the remaining queued sessions run in their original
494
+ order; already-completed sessions are skipped. Returns 202 immediately;
495
+ this method polls GET /api/v1/orchestrate-auto/status until terminal.
496
+
497
+ on_status_update: optional async callable(auto_status: str, state: dict).
498
+ """
499
+ url = self._url("/api/v1/orchestrate-auto/resume")
500
+ try:
501
+ async with httpx.AsyncClient(timeout=30.0) as client:
502
+ resp = await client.post(url, headers=self._headers)
503
+ _check_status(resp.status_code, url)
504
+ except (BackendAuthFailed, BackendVersionTooOld):
505
+ raise
506
+ except Exception as exc:
507
+ raise BackendUnreachable(self._base_url, str(exc)) from exc
508
+
509
+ return await self._poll_auto_terminal(on_status_update)
510
+
511
+ async def _orchestrate_auto_status(self) -> dict:
512
+ """GET /api/v1/orchestrate-auto/status"""
513
+ async with httpx.AsyncClient(timeout=15.0) as client:
514
+ resp = await client.get(
515
+ self._url("/api/v1/orchestrate-auto/status"),
516
+ headers=self._headers,
517
+ )
518
+ _check_status(resp.status_code, self._base_url)
519
+ return resp.json()
520
+
283
521
  async def approve_task(self, task_id: str, decision: str, note: str = "") -> dict:
284
522
  async with httpx.AsyncClient(timeout=15.0) as client:
285
523
  resp = await client.post(
@@ -290,6 +528,42 @@ class BackendClient:
290
528
  _check_status(resp.status_code, self._base_url)
291
529
  return resp.json()
292
530
 
531
+ async def approval_reply(
532
+ self,
533
+ task_id: str,
534
+ decision: str = "APPROVED",
535
+ *,
536
+ local_execution_result: dict | None = None,
537
+ approver_role: str = "TEAM_LEAD",
538
+ approver_name: str = "Desktop",
539
+ raw_reply: str = "",
540
+ ) -> dict:
541
+ """POST /api/v1/webhooks/approval-reply — resume a System-B run after approval.
542
+
543
+ Auth uses the same co_ token (accepted by the endpoint's task-token path).
544
+ For the local-execution hybrid, pass ``local_execution_result`` so the
545
+ server injects the desktop's locally-produced result instead of
546
+ dispatching agents on the server.
547
+ """
548
+ body: dict = {
549
+ "taskId": task_id,
550
+ "approvalId": "desktop-local",
551
+ "decision": decision,
552
+ "approverRole": approver_role,
553
+ "approverName": approver_name,
554
+ "rawReply": raw_reply or f"{decision} (desktop local execution)",
555
+ }
556
+ if local_execution_result is not None:
557
+ body["localExecutionResult"] = local_execution_result
558
+ async with httpx.AsyncClient(timeout=20.0) as client:
559
+ resp = await client.post(
560
+ self._url("/api/v1/webhooks/approval-reply"),
561
+ json=body,
562
+ headers=self._headers,
563
+ )
564
+ _check_status(resp.status_code, self._base_url)
565
+ return resp.json()
566
+
293
567
  # ── Tasks ──────────────────────────────────────────────────────────
294
568
 
295
569
  async def list_tasks(
@@ -115,6 +115,9 @@ def get_token() -> str:
115
115
 
116
116
  def configure_cli(rotate: bool = False) -> None:
117
117
  """Interactive `agentos configure` — paste your apiTokenHash once."""
118
+ if not rotate:
119
+ rotate = "--rotate" in sys.argv
120
+
118
121
  print("AgentOS SDK — Configuration")
119
122
  print("=" * 40)
120
123