devops-bot-sdk 1.4.20__tar.gz → 1.4.23__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 (38) hide show
  1. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/client.py +1 -1
  6. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/ipc/handlers.py +50 -4
  7. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/local_exec.py +36 -0
  8. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/README.md +0 -0
  9. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  10. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  11. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  12. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/devops_bot_sdk.egg-info/requires.txt +0 -0
  13. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  14. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/collectors/__init__.py +0 -0
  15. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/collectors/files.py +0 -0
  16. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/collectors/process.py +0 -0
  17. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/collectors/screenshot.py +0 -0
  18. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/config.py +0 -0
  19. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/crucial.py +0 -0
  20. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/exceptions.py +0 -0
  21. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/git_ops.py +0 -0
  22. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/graphify.py +0 -0
  23. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/hooks/__init__.py +0 -0
  24. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/hooks/crucial_guard.py +0 -0
  25. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/ipc/__init__.py +0 -0
  26. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/ipc/electron_bridge.py +0 -0
  27. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/models/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/models/envelope.py +0 -0
  29. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/models/requests.py +0 -0
  30. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/models/responses.py +0 -0
  31. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/models/snapshots.py +0 -0
  32. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/py.typed +0 -0
  33. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/run_auto.py +0 -0
  34. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.20 → devops_bot_sdk-1.4.23}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.20
3
+ Version: 1.4.23
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.4.20
3
+ Version: 1.4.23
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devops-bot-sdk"
7
- version = "1.4.20"
7
+ version = "1.4.23"
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.4.20
3
+ Version: 1.4.23
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.4.20"
33
+ __version__ = "1.4.23"
34
34
  __author__ = "AgentOS"
35
35
 
36
36
  from sdk.client import BackendClient
@@ -36,7 +36,7 @@ from sdk.sse import _check_status, stream_with_reconnect
36
36
 
37
37
  logger = logging.getLogger(__name__)
38
38
 
39
- SDK_VERSION = "1.4.20"
39
+ SDK_VERSION = "1.4.23"
40
40
  _POLL_INTERVAL = 3.0
41
41
  _POLL_TIMEOUT = 600.0
42
42
  _ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
@@ -296,8 +296,14 @@ async def _run_local_pipeline(
296
296
  body["user_id"] = derived
297
297
 
298
298
  # 1. Kick off the background run (server plans, gates — does NOT execute).
299
+ # The kickoff sends background=True and should return a session_id quickly, but
300
+ # under load the backend can take well over 30s to ack — which previously raised
301
+ # a (blank-message) ReadTimeout that sent the whole ticket to Backlog. Connect
302
+ # fast, but allow a generous read window so a slow-but-working ack isn't killed.
299
303
  import httpx
300
- async with httpx.AsyncClient(timeout=30.0) as http:
304
+ _kick_read_s = float(os.getenv("AGENTOS_ORCHESTRATE_KICKOFF_TIMEOUT_S", "") or 120.0)
305
+ _kick_timeout = httpx.Timeout(_kick_read_s, connect=15.0)
306
+ async with httpx.AsyncClient(timeout=_kick_timeout) as http:
301
307
  resp = await http.post(client._url("/api/v1/orchestrate"), json=body, headers=client._headers)
302
308
  if resp.status_code >= 400:
303
309
  await send(_error_envelope(thread_id, RuntimeError(f"orchestrate failed: {resp.status_code} {resp.text[:200]}")).model_dump())
@@ -560,6 +566,36 @@ async def _code_one_repo(
560
566
  )
561
567
  session_id = result.get("session_id") or session_id
562
568
 
569
+ # 4b. output-token-cap recovery. A single assistant turn that would exceed the
570
+ # CLI's per-response output cap aborts the run ("response exceeded the N output
571
+ # token maximum"). Resume the session with a doubled cap and an instruction to
572
+ # emit the work in smaller chunks, so a large dump finishes across turns instead
573
+ # of failing the ticket.
574
+ cap_resumes = 0
575
+ next_cap = local_exec._MAX_OUTPUT_TOKENS
576
+ while (not result.get("ok")) and result.get("output_cap_exceeded") \
577
+ and session_id and cap_resumes < _MAX_OUTPUT_CAP_RESUMES:
578
+ cap_resumes += 1
579
+ next_cap = min(next_cap * 2, 128000) # CLI/model output ceiling
580
+ await send(Envelope(
581
+ type="step_update", thread_id=thread_id,
582
+ data={"status": "running",
583
+ "current_step": f"[{name}] Response exceeded output-token cap — "
584
+ f"resuming with cap {next_cap} and chunked output "
585
+ f"(attempt {cap_resumes}/{_MAX_OUTPUT_CAP_RESUMES})"},
586
+ ).model_dump())
587
+ result = await local_exec.run_claude_local(
588
+ "Your previous response was cut off because it exceeded the output-token "
589
+ "limit for a single turn. Continue from where you left off and finish the "
590
+ "task. Keep each response within the limit: write results incrementally to "
591
+ "files in smaller steps rather than emitting one very large response, and "
592
+ "avoid dumping large content back as chat text.",
593
+ repo_path, on_event=_on_event,
594
+ github_token=gh_token, resume_session_id=session_id,
595
+ approval_dir=approval_dir, max_output_tokens=next_cap,
596
+ )
597
+ session_id = result.get("session_id") or session_id
598
+
563
599
  # 5. mid-run crucial-decision gate
564
600
  if agent_mode and session_id:
565
601
  from sdk import crucial
@@ -1529,6 +1565,9 @@ _FIRST_GATE_TIMEOUT_S = 900.0
1529
1565
  _APPROVAL_POLL_TIMEOUT_S = 1200.0
1530
1566
  # Max times a single ticket's session is resumed after a context overflow.
1531
1567
  _MAX_CONTEXT_RESUMES = 3
1568
+ # Max times a ticket's session is resumed after hitting the per-response output-token
1569
+ # cap, each time with a doubled cap, to let a large turn finish across chunks.
1570
+ _MAX_OUTPUT_CAP_RESUMES = 3
1532
1571
  # Max mid-run crucial-decision approval gates per ticket (safety bound).
1533
1572
  _MAX_CRUCIAL_GATES = 10
1534
1573
  # Max times a ticket is retried after hitting the Claude usage limit.
@@ -1843,12 +1882,19 @@ async def handle_orchestrate_auto_local_run(
1843
1882
  processed += 1
1844
1883
  break
1845
1884
  except Exception as exc: # noqa: BLE001 — isolate per-ticket failures
1846
- logger.warning("orchestrate_auto.task_failed", extra={"task_id": tid, "error": str(exc)})
1847
- await client.log_activity(f"Task {tid}: unexpected error {str(exc)[:200]}")
1885
+ # str(exc) is EMPTY for some exceptions (notably TimeoutError /
1886
+ # asyncio.TimeoutError / CancelledError), which used to render as a
1887
+ # blank "failed — (continuing)" with no clue to the cause. Always
1888
+ # prefix the exception type so the real reason is visible; log the
1889
+ # full repr for after-the-fact debugging.
1890
+ detail = f"{type(exc).__name__}: {exc}".rstrip(": ")
1891
+ logger.warning("orchestrate_auto.task_failed",
1892
+ extra={"task_id": tid, "error": repr(exc)})
1893
+ await client.log_activity(f"Task {tid}: unexpected error — {detail[:200]}")
1848
1894
  await client.move_to_backlog(tid)
1849
1895
  await send(Envelope(
1850
1896
  type="step_update", thread_id=thread_id,
1851
- data={"current_step": f"{jira_key or tid}: failed — {str(exc)[:160]} (continuing)"},
1897
+ data={"current_step": f"{jira_key or tid}: failed — {detail[:160]} (continuing)"},
1852
1898
  ).model_dump())
1853
1899
  continue
1854
1900
 
@@ -62,6 +62,23 @@ _CONTEXT_MARKERS = (
62
62
  "token limit", "prompt is too long", "exceeds the maximum", "context_length_exceeded",
63
63
  )
64
64
 
65
+ # Markers that indicate the CLI's per-response OUTPUT-token cap was hit — distinct
66
+ # from a context overflow (input side) and a usage/rate limit (account side). The
67
+ # CLI aborts a single assistant turn that would exceed CLAUDE_CODE_MAX_OUTPUT_TOKENS
68
+ # (default 32000). We raise that cap below and, if it's still hit, resume the
69
+ # session to finish the turn in smaller chunks.
70
+ _OUTPUT_CAP_MARKERS = (
71
+ "output token maximum", "max_output_tokens", "claude_code_max_output_tokens",
72
+ "max output tokens", "output_tokens exceed",
73
+ )
74
+
75
+ # Per-response output-token cap handed to the CLI via CLAUDE_CODE_MAX_OUTPUT_TOKENS.
76
+ # The CLI default is 32000, which a large single dump (e.g. extracting a whole
77
+ # taxonomy from a spreadsheet) can blow past, failing the whole ticket. 64000 is a
78
+ # safe headroom for the Opus/Sonnet families. Env-tunable; a value the user already
79
+ # exported in the environment is respected and never overridden.
80
+ _MAX_OUTPUT_TOKENS = int(_f("AGENTOS_MAX_OUTPUT_TOKENS", 64000.0))
81
+
65
82
  # Markers that indicate the Claude USAGE/RATE limit (the N-hour window) was hit —
66
83
  # distinct from a context overflow. The run didn't fail on the task; the account
67
84
  # is throttled until the window resets.
@@ -162,6 +179,12 @@ def _ctx_exceeded(text: str | None) -> bool:
162
179
  low = (text or "").lower()
163
180
  return any(m in low for m in _CONTEXT_MARKERS)
164
181
 
182
+
183
+ def _output_cap_exceeded(text: str | None) -> bool:
184
+ """True when the CLI aborted because the response hit the per-turn output cap."""
185
+ low = (text or "").lower()
186
+ return any(m in low for m in _OUTPUT_CAP_MARKERS)
187
+
165
188
  EventHandler = Callable[[dict], Awaitable[None]]
166
189
 
167
190
 
@@ -186,6 +209,7 @@ async def run_claude_local(
186
209
  resume_session_id: str | None = None,
187
210
  approval_dir: str | None = None,
188
211
  idle_timeout: float = _IDLE_TIMEOUT_S,
212
+ max_output_tokens: int | None = None,
189
213
  ) -> dict[str, Any]:
190
214
  """Run the local `claude` CLI non-interactively against ``project_path``.
191
215
 
@@ -258,6 +282,16 @@ async def run_claude_local(
258
282
  # `git clone https://github.com/owner/repo` authenticates — without touching
259
283
  # the user's global git config. Tokens stay in the subprocess env only.
260
284
  env = dict(os.environ)
285
+
286
+ # Raise the CLI's per-response output-token cap. Without this the CLI errors
287
+ # out ("Claude's response exceeded the 32000 output token maximum") the moment
288
+ # a single assistant turn would exceed 32000 tokens — a large dump (extracting
289
+ # a whole taxonomy, generating many files at once) fails the entire ticket.
290
+ # A value the operator already exported wins; the `max_output_tokens` override
291
+ # lets a retry escalate the cap after a cap-exceeded run (see handlers.py).
292
+ if "CLAUDE_CODE_MAX_OUTPUT_TOKENS" not in env or max_output_tokens:
293
+ env["CLAUDE_CODE_MAX_OUTPUT_TOKENS"] = str(max_output_tokens or _MAX_OUTPUT_TOKENS)
294
+
261
295
  if github_token:
262
296
  env["GH_TOKEN"] = github_token
263
297
  env["GITHUB_TOKEN"] = github_token
@@ -419,6 +453,7 @@ async def run_claude_local(
419
453
  "cost_usd": 0.0, "files_changed": files_changed,
420
454
  "session_id": session_id, "context_exceeded": _ctx_exceeded(err),
421
455
  "limit_exceeded": _is_limit(err), "limit_reset_epoch": _parse_reset_epoch(err),
456
+ "output_cap_exceeded": _output_cap_exceeded(err),
422
457
  })
423
458
 
424
459
  usage = result_event.get("usage") or {}
@@ -453,4 +488,5 @@ async def run_claude_local(
453
488
  "context_exceeded": is_error and _ctx_exceeded(err_text),
454
489
  "limit_exceeded": limit_hit,
455
490
  "limit_reset_epoch": reset_epoch,
491
+ "output_cap_exceeded": is_error and _output_cap_exceeded(err_text),
456
492
  })