sbxloop 0.5.1__tar.gz → 0.5.3__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 (43) hide show
  1. {sbxloop-0.5.1 → sbxloop-0.5.3}/PKG-INFO +1 -1
  2. sbxloop-0.5.1/src/sbxloop/_vendor/sbxloop_worker-0.5.1-py3-none-any.whl → sbxloop-0.5.3/src/sbxloop/_vendor/sbxloop_worker-0.5.3-py3-none-any.whl +0 -0
  3. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/_version.py +2 -2
  4. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/engine.py +26 -7
  5. sbxloop-0.5.3/src/sbxloop/gh/reporter.py +99 -0
  6. sbxloop-0.5.1/src/sbxloop/gh/reporter.py +0 -76
  7. {sbxloop-0.5.1 → sbxloop-0.5.3}/.gitignore +0 -0
  8. {sbxloop-0.5.1 → sbxloop-0.5.3}/README.md +0 -0
  9. {sbxloop-0.5.1 → sbxloop-0.5.3}/hatch_build.py +0 -0
  10. {sbxloop-0.5.1 → sbxloop-0.5.3}/pyproject.toml +0 -0
  11. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/__init__.py +0 -0
  12. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/cli/__init__.py +0 -0
  13. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/cli/app.py +0 -0
  14. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/cli/doctor.py +0 -0
  15. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/cli/tui.py +0 -0
  16. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/config.py +0 -0
  17. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/deliver.py +0 -0
  18. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/__init__.py +0 -0
  19. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/model.py +0 -0
  20. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/phases.py +0 -0
  21. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/prompts/__init__.py +0 -0
  22. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/prompts/decompose.md +0 -0
  23. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/prompts/execute.md +0 -0
  24. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/prompts/plan.md +0 -0
  25. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/prompts/scrutinize.md +0 -0
  26. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/prompts/validate.md +0 -0
  27. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/store.py +0 -0
  28. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/errors.py +0 -0
  29. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/events.py +0 -0
  30. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/gh/__init__.py +0 -0
  31. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/gh/ops.py +0 -0
  32. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/ids.py +0 -0
  33. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/py.typed +0 -0
  34. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/__init__.py +0 -0
  35. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/cli.py +0 -0
  36. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/models.py +0 -0
  37. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/pair.py +0 -0
  38. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/parse.py +0 -0
  39. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/provision.py +0 -0
  40. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/sandbox.py +0 -0
  41. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/worker/__init__.py +0 -0
  42. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/worker/client.py +0 -0
  43. {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/worker/wheel.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sbxloop
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: Agentic loop orchestration on Docker Sandboxes (sbx) with isolated credential domains
5
5
  Project-URL: Homepage, https://github.com/brettbergin/sbxloop
6
6
  Project-URL: Repository, https://github.com/brettbergin/sbxloop
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.5.1'
22
- __version_tuple__ = version_tuple = (0, 5, 1)
21
+ __version__ = version = '0.5.3'
22
+ __version_tuple__ = version_tuple = (0, 5, 3)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -32,8 +32,6 @@ from sbxloop.engine.phases import PhaseRunner, clip
32
32
  from sbxloop.engine.store import StateStore
33
33
  from sbxloop.errors import (
34
34
  BudgetExceededError,
35
- DeliveryError,
36
- GithubOpsError,
37
35
  SbxError,
38
36
  SdxloopError,
39
37
  StateError,
@@ -139,12 +137,17 @@ class LoopEngine:
139
137
  agent.install(extras="copilot", ensure_dev_tools=True)
140
138
  if github is not None:
141
139
  github.install(extras="")
142
- detach = self._attach_reporter(github, run_id)
140
+ reporter, detach = self._attach_reporter(github, run_id, outcome)
143
141
  try:
144
142
  phases = PhaseRunner(
145
143
  agent, self.config, run_id, outcome, workdir=pair.agent_workdir
146
144
  )
147
145
  state = self._run_phases(run_id, phases, deadline, pair)
146
+ # Summary must post while the github sandbox is alive;
147
+ # on an infra exception the run is resumable and the
148
+ # resumed run reopens the same tracking issue.
149
+ if reporter is not None:
150
+ reporter.close_run(run_id, state)
148
151
  finally:
149
152
  detach()
150
153
  # Harvest even when a phase raised: the sandbox is still
@@ -167,13 +170,24 @@ class LoopEngine:
167
170
  mounted=pair.mounted,
168
171
  )
169
172
 
170
- def _attach_reporter(self, github: WorkerClient | None, run_id: str) -> Callable[[], None]:
173
+ def _attach_reporter(
174
+ self, github: WorkerClient | None, run_id: str, outcome: str
175
+ ) -> tuple[GithubReporterHook | None, Callable[[], None]]:
176
+ """Attach progress reporting; opens the tracking issue immediately.
177
+
178
+ Run start/end go through explicit ``open_run``/``close_run`` calls
179
+ rather than bus events: RUN_START is emitted before the github
180
+ sandbox exists and RUN_END after it is gone, so the hook could never
181
+ observe them (#58).
182
+ """
171
183
  gh = self.config.github
172
184
  if not gh.report or github is None:
173
- return lambda: None
185
+ return None, lambda: None
174
186
  assert gh.repo is not None # report=True without a repo cannot provision a github worker
175
187
  hook = GithubReporterHook(GithubOps(github, run_id), gh.repo)
176
- return self.bus.attach_hook(hook)
188
+ detach = self.bus.attach_hook(hook)
189
+ hook.open_run(run_id, outcome)
190
+ return hook, detach
177
191
 
178
192
  def _harvest(self, run_id: str, pair: SandboxPair) -> None:
179
193
  """Copy the in-VM work dir out to the host (unmounted runs only).
@@ -240,7 +254,12 @@ class LoopEngine:
240
254
  base=gh.deliver_base,
241
255
  draft=gh.deliver_draft,
242
256
  )
243
- except (DeliveryError, GithubOpsError) as exc:
257
+ except SdxloopError as exc:
258
+ # Catches the whole family the delivery path can raise — not just
259
+ # DeliveryError/GithubOpsError but WorkerError/WorkerTimeoutError/
260
+ # SbxError from the op jobs themselves. Anything narrower lets an
261
+ # infra hiccup during this optional post-completion step escape
262
+ # _drive and leave the completed run looking failed (#59).
244
263
  logger.warning("delivery to %s failed for run %s", repo, run_id, exc_info=True)
245
264
  self.bus.emit(HostEventTypes.RUN_DELIVER, run_id, repo=repo, error=str(exc))
246
265
  return
@@ -0,0 +1,99 @@
1
+ """GithubReporterHook: mirrors run progress to a GitHub tracking issue.
2
+
3
+ The default consumer of the github-ops sandbox. When the GitHub integration
4
+ is configured (``[github].repo``) and reporting is enabled (``report = true``
5
+ or ``--report``), the engine opens a tracking issue right after the github
6
+ sandbox is ready, comments as tasks finish, and posts a final summary while
7
+ the sandbox is still alive.
8
+
9
+ Run start/end are **explicit calls** (``open_run``/``close_run``) rather
10
+ than bus events: the run lifecycle events are emitted outside the window in
11
+ which the hook is attached and the github sandbox exists, so subscribing to
12
+ them can never work — see #58. Only per-task progress arrives via the bus.
13
+
14
+ Every entry point is guarded: a reporting failure must not fail a run — and
15
+ the EventBus isolates ``on_event`` anyway.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+
22
+ from sbxloop.events import Event, HostEventTypes
23
+ from sbxloop.gh.ops import GithubOps, IssueRef
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class GithubReporterHook:
29
+ def __init__(self, ops: GithubOps, repo: str) -> None:
30
+ self.ops = ops
31
+ self.repo = repo
32
+ self.issue: IssueRef | None = None
33
+ self._task_lines: list[str] = []
34
+
35
+ # -- explicit lifecycle (called by the engine, sandbox guaranteed alive) --
36
+
37
+ def open_run(self, run_id: str, outcome: str) -> None:
38
+ """Open (or on resume, re-find) the run's tracking issue."""
39
+ try:
40
+ self.issue = self._find_existing(run_id) or self._create(run_id, outcome)
41
+ except Exception:
42
+ logger.warning("github reporting: opening tracking issue failed", exc_info=True)
43
+
44
+ def close_run(self, run_id: str, state: str) -> None:
45
+ """Post the final summary comment; call before sandbox teardown."""
46
+ if self.issue is None:
47
+ return
48
+ summary = "\n".join(self._task_lines) or "_no tasks were executed_"
49
+ try:
50
+ self.ops.issue_comment(
51
+ self.repo,
52
+ self.issue.number,
53
+ f"Run `{run_id}` finished: **{state}**\n\n{summary}",
54
+ )
55
+ except Exception:
56
+ logger.warning("github reporting: final summary failed", exc_info=True)
57
+
58
+ # -- Hook protocol (task progress only) ----------------------------------
59
+
60
+ def on_event(self, event: Event) -> None:
61
+ try:
62
+ if event.type == HostEventTypes.TASK_END:
63
+ self._on_task_end(event)
64
+ except Exception:
65
+ logger.warning("github reporting failed for %s", event.type, exc_info=True)
66
+
67
+ # -- internals -----------------------------------------------------------
68
+
69
+ def _find_existing(self, run_id: str) -> IssueRef | None:
70
+ """A resumed run reuses its issue instead of opening a duplicate."""
71
+ query = f'repo:{self.repo} is:issue in:title "sbxloop run {run_id}"'
72
+ for item in self.ops.search_issues(query, per_page=5):
73
+ if item.get("title") == f"sbxloop run {run_id}" and item.get("number"):
74
+ return IssueRef(number=int(item["number"]), url=str(item.get("html_url", "")))
75
+ return None
76
+
77
+ def _create(self, run_id: str, outcome: str) -> IssueRef:
78
+ body = (
79
+ f"sbxloop run `{run_id}` started.\n\n"
80
+ f"**Outcome:**\n\n> {outcome}\n\n"
81
+ "Progress is reported as comments on this issue."
82
+ )
83
+ return self.ops.issue_create(
84
+ self.repo,
85
+ title=f"sbxloop run {run_id}",
86
+ body=body,
87
+ labels=["sbxloop"],
88
+ )
89
+
90
+ def _on_task_end(self, event: Event) -> None:
91
+ if self.issue is None:
92
+ return
93
+ task_id = event.data.get("task_id", "?")
94
+ title = event.data.get("title", "")
95
+ state = event.data.get("state", "?")
96
+ marker = "✅" if state == "done" else "❌"
97
+ line = f"{marker} `{task_id}` {title} — **{state}**"
98
+ self._task_lines.append(line)
99
+ self.ops.issue_comment(self.repo, self.issue.number, line)
@@ -1,76 +0,0 @@
1
- """GithubReporterHook: mirrors run progress to a GitHub tracking issue.
2
-
3
- The default consumer of the github-ops sandbox. When the GitHub integration
4
- is configured (``[github].repo``) and reporting is enabled (``report = true``
5
- or ``--report``), the hook opens a tracking issue at run start, comments as
6
- tasks finish, and posts a final summary at run end. It never raises — a
7
- reporting failure must not fail a run — and the EventBus isolates it anyway.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import logging
13
-
14
- from sbxloop.events import Event, HostEventTypes
15
- from sbxloop.gh.ops import GithubOps, IssueRef
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class GithubReporterHook:
21
- def __init__(self, ops: GithubOps, repo: str) -> None:
22
- self.ops = ops
23
- self.repo = repo
24
- self.issue: IssueRef | None = None
25
- self._task_lines: list[str] = []
26
-
27
- # -- Hook protocol -----------------------------------------------------
28
-
29
- def on_event(self, event: Event) -> None:
30
- try:
31
- if event.type == HostEventTypes.RUN_START:
32
- self._on_run_start(event)
33
- elif event.type == HostEventTypes.TASK_END:
34
- self._on_task_end(event)
35
- elif event.type == HostEventTypes.RUN_END:
36
- self._on_run_end(event)
37
- except Exception:
38
- logger.warning("github reporting failed for %s", event.type, exc_info=True)
39
-
40
- # -- handlers ----------------------------------------------------------
41
-
42
- def _on_run_start(self, event: Event) -> None:
43
- outcome = str(event.data.get("outcome", ""))
44
- body = (
45
- f"sbxloop run `{event.run_id}` started.\n\n"
46
- f"**Outcome:**\n\n> {outcome}\n\n"
47
- "Progress is reported as comments on this issue."
48
- )
49
- self.issue = self.ops.issue_create(
50
- self.repo,
51
- title=f"sbxloop run {event.run_id}",
52
- body=body,
53
- labels=["sbxloop"],
54
- )
55
-
56
- def _on_task_end(self, event: Event) -> None:
57
- if self.issue is None:
58
- return
59
- task_id = event.data.get("task_id", "?")
60
- title = event.data.get("title", "")
61
- state = event.data.get("state", "?")
62
- marker = "✅" if state == "done" else "❌"
63
- line = f"{marker} `{task_id}` {title} — **{state}**"
64
- self._task_lines.append(line)
65
- self.ops.issue_comment(self.repo, self.issue.number, line)
66
-
67
- def _on_run_end(self, event: Event) -> None:
68
- if self.issue is None:
69
- return
70
- state = event.data.get("state", "?")
71
- summary = "\n".join(self._task_lines) or "_no tasks were executed_"
72
- self.ops.issue_comment(
73
- self.repo,
74
- self.issue.number,
75
- f"Run `{event.run_id}` finished: **{state}**\n\n{summary}",
76
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes