devops-bot-sdk 1.4.92__tar.gz → 1.4.96__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.92 → devops_bot_sdk-1.4.96}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/client.py +11 -3
  6. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/ipc/handlers.py +42 -7
  7. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/local_exec.py +15 -0
  8. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/README.md +0 -0
  9. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  10. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  11. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  12. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/devops_bot_sdk.egg-info/requires.txt +0 -0
  13. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  14. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/collectors/__init__.py +0 -0
  15. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/collectors/files.py +0 -0
  16. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/collectors/process.py +0 -0
  17. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/collectors/screenshot.py +0 -0
  18. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/config.py +0 -0
  19. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/crucial.py +0 -0
  20. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/exceptions.py +0 -0
  21. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/git_ops.py +0 -0
  22. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/graphify.py +0 -0
  23. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/hooks/__init__.py +0 -0
  24. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/hooks/crucial_guard.py +0 -0
  25. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/ipc/__init__.py +0 -0
  26. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/ipc/electron_bridge.py +0 -0
  27. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/models/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/models/envelope.py +0 -0
  29. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/models/requests.py +0 -0
  30. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/models/responses.py +0 -0
  31. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/models/snapshots.py +0 -0
  32. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/py.typed +0 -0
  33. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/run_auto.py +0 -0
  34. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.92 → devops_bot_sdk-1.4.96}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.92
3
+ Version: 1.4.96
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.92
3
+ Version: 1.4.96
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.92"
7
+ version = "1.4.96"
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.92
3
+ Version: 1.4.96
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.92"
33
+ __version__ = "1.4.96"
34
34
  __author__ = "AgentOS"
35
35
 
36
36
  from sdk.client import BackendClient
@@ -58,7 +58,7 @@ def _notify_status(task_id: str, status: str) -> None:
58
58
  pass
59
59
 
60
60
 
61
- SDK_VERSION = "1.4.92"
61
+ SDK_VERSION = "1.4.96"
62
62
  _POLL_INTERVAL = 3.0
63
63
  _POLL_TIMEOUT = 600.0
64
64
  _ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
@@ -849,9 +849,17 @@ class BackendClient:
849
849
  headers = {k: v for k, v in self._headers.items() if k.lower() != "content-type"}
850
850
  headers.update({"accept": "*/*", "developer-name": "null", "product": ""})
851
851
  # POST /tasks-ml must never carry an SLA target — the backend validator
852
- # rejects any value not shaped like "8h", and SLA is not ours to set.
852
+ # rejects any value not shaped like "8h" (an empty "" 400s with
853
+ # "Slatarget must be a positive integer of hours…"), and SLA is not ours
854
+ # to set. Drop it by NORMALISED name so every casing/separator variant
855
+ # (slaTarget, slaTargets, sla_target, SLATarget, sla-target, …) is
856
+ # excluded — never sent as "" or anything else, always an empty field.
857
+ def _is_sla(key: str) -> bool:
858
+ norm = "".join(ch for ch in key.lower() if ch.isalnum())
859
+ return norm in ("slatarget", "slatargets")
860
+
853
861
  files = [(k, (None, str(v))) for k, v in fields.items()
854
- if v is not None and k not in ("slaTarget", "slaTargets")]
862
+ if v is not None and not _is_sla(k)]
855
863
  for m in (model_used or []):
856
864
  files.append(("modelUsed[]", (None, str(m)))) # [] → backend parses as array
857
865
  try:
@@ -416,7 +416,10 @@ async def _run_with_heartbeat(coro, *, send: Callable, thread_id: str, label: st
416
416
  beat.cancel()
417
417
  try:
418
418
  await beat
419
- except Exception: # noqa: BLE001
419
+ except (asyncio.CancelledError, Exception): # noqa: BLE001
420
+ # CancelledError is a BaseException: raised here when the beat task
421
+ # was cancelled before it ever ran (coro finished immediately) — it
422
+ # must not propagate and sink the caller's result.
420
423
  pass
421
424
 
422
425
 
@@ -441,6 +444,7 @@ async def _run_generation(
441
444
  send=send, thread_id=thread_id, label=label, name=name,
442
445
  )
443
446
  combined = res.get("result") or ""
447
+ longest = res.get("longest_text") or ""
444
448
  sid = res.get("session_id")
445
449
  chunks = 0
446
450
  while res.get("output_cap_exceeded") and sid and chunks < _MAX_OUTPUT_CAP_RESUMES:
@@ -463,9 +467,24 @@ async def _run_generation(
463
467
  send=send, thread_id=thread_id, label=f"{label} (cont.)", name=name,
464
468
  )
465
469
  combined += res.get("result") or ""
470
+ if len(res.get("longest_text") or "") > len(longest):
471
+ longest = res.get("longest_text") or ""
466
472
  sid = res.get("session_id") or sid
467
473
  out = dict(res)
468
474
  out["result"] = combined # the full, reassembled output
475
+ # Salvage a lost artifact: the CLI's `result` is only the LAST assistant
476
+ # message. If the model wrote the artifact and was then woken once more by a
477
+ # stray late event (e.g. a leftover background timer) for a short closing
478
+ # remark ("nothing further is pending"), `combined` is that remark and the
479
+ # real output is an EARLIER, much longer message. When an earlier message
480
+ # dwarfs the final one, it is the artifact — use it.
481
+ if (not out.get("limit_exceeded") and len(longest) > 1000
482
+ and len(longest) > 2 * len(combined.strip())):
483
+ logger.warning(
484
+ "generation.salvaged_longest_message final_len=%s longest_len=%s",
485
+ len(combined.strip()), len(longest),
486
+ )
487
+ out["result"] = longest.strip()
469
488
  out["session_id"] = sid
470
489
  return out
471
490
 
@@ -1336,13 +1355,22 @@ async def _handle_story(
1336
1355
  # The Story is complete — nothing to convert or code in this run.
1337
1356
  return None
1338
1357
 
1339
- # STEP 1 — validate whether this is a CODEBASE (development) task: repo links /
1340
- # file paths in the instructions, or dev signals (PRs, branches, endpoints,
1341
- # deploy, …). A codebase task ALWAYS gets a technical PRD built from the current
1342
- # codebase; other domains (Finance, HR, …) get one only when explicitly asked.
1358
+ # STEP 1 — do we have CODEBASE UNDERSTANDING to ground a PRD in? That means an
1359
+ # actual repo target (a github/gitlab/bitbucket link in the instructions OR the
1360
+ # story), or dev work called for in the OPERATOR INSTRUCTIONS (PRs, branches,
1361
+ # endpoints, deploy, …).
1362
+ #
1363
+ # Deliberately NOT keyed off the story DESCRIPTION's vocabulary or a "PRD"
1364
+ # mention: a business/BA user story that merely contains technical words
1365
+ # (endpoint, API, database, "PRD", …) is NOT a codebase task. Keying off the
1366
+ # description used to false-positive such stories into PRD generation, which
1367
+ # folded an unwanted PRD into — and mangled — the operator's own document.
1343
1368
  has_repo = bool(_extract_repo_targets(human)) or bool(_extract_repo_targets(user_story))
1344
- is_codebase_task = has_repo or _is_dev_task(human) or _is_dev_task(user_story)
1345
- wants_prd = is_codebase_task or _wants_prd(human) or _wants_prd(user_story)
1369
+ is_codebase_task = has_repo or _is_dev_task(human)
1370
+ # PRD ONLY when we have codebase understanding. Without a repo/codebase, the
1371
+ # Story is converted to a Task with its description left intact (the whole,
1372
+ # proper document the operator provided) — never rewritten with a PRD.
1373
+ wants_prd = is_codebase_task
1346
1374
 
1347
1375
  if is_codebase_task:
1348
1376
  step = ("validated as a codebase task — building a technical PRD from the "
@@ -1382,6 +1410,13 @@ async def _handle_story(
1382
1410
  # Use the output regardless of an is_error flag (a stray read-only tool denial
1383
1411
  # shouldn't discard a PRD the model actually produced); only a usage limit is fatal.
1384
1412
  prd = "" if gen.get("limit_exceeded") else (gen.get("result") or "").strip()
1413
+ if prd and len(prd) < 400:
1414
+ # A real PRD (numbered sections per _PRD_FORMAT) is never this short —
1415
+ # this is a stub (e.g. a closing remark captured as the run's result).
1416
+ # Treat it as a failed generation rather than filing garbage in the ticket.
1417
+ if not gen.get("error"):
1418
+ gen["error"] = f"PRD too short ({len(prd)} chars) — not a document"
1419
+ prd = ""
1385
1420
  if prd:
1386
1421
  # PRD lives alongside the user story in the (now Task) description.
1387
1422
  new_description = f"{user_story}\n\n---\n\n{_PRD_MARKER}\n\n{prd}"[:20000]
@@ -386,6 +386,12 @@ async def run_claude_local(
386
386
 
387
387
  result_event: dict | None = None
388
388
  files_changed: list[str] = []
389
+ # Longest single assistant TEXT message seen in the stream. The CLI's final
390
+ # `result` field is only the LAST assistant message — if the model outputs a
391
+ # large artifact (PRD/decomposition) and is then woken once more by a stray
392
+ # late event (e.g. a leftover background timer) for a short closing remark,
393
+ # the artifact would be lost. Callers use this to salvage it.
394
+ longest_text = {"text": ""}
389
395
  session_id: str | None = resume_session_id
390
396
  import time as _time
391
397
  started = _time.monotonic()
@@ -452,6 +458,14 @@ async def run_claude_local(
452
458
  if event.get("session_id"):
453
459
  session_id = str(event["session_id"])
454
460
 
461
+ # Track the longest assistant text message (see `longest_text` above).
462
+ if event.get("type") == "assistant":
463
+ for c in event.get("message", {}).get("content", []) or []:
464
+ if isinstance(c, dict) and c.get("type") == "text":
465
+ txt = str(c.get("text") or "")
466
+ if len(txt) > len(longest_text["text"]):
467
+ longest_text["text"] = txt
468
+
455
469
  # Track files the agent created/edited for the result summary.
456
470
  if event.get("type") == "user":
457
471
  tr = event.get("tool_use_result")
@@ -526,6 +540,7 @@ async def run_claude_local(
526
540
  return _finish({
527
541
  "ok": (not is_error) and (not limit_hit),
528
542
  "result": result_text,
543
+ "longest_text": longest_text["text"],
529
544
  "input_tokens": in_tok,
530
545
  "output_tokens": out_tok,
531
546
  "total_tokens": in_tok + out_tok,