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.
- {sbxloop-0.5.1 → sbxloop-0.5.3}/PKG-INFO +1 -1
- 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
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/_version.py +2 -2
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/engine.py +26 -7
- sbxloop-0.5.3/src/sbxloop/gh/reporter.py +99 -0
- sbxloop-0.5.1/src/sbxloop/gh/reporter.py +0 -76
- {sbxloop-0.5.1 → sbxloop-0.5.3}/.gitignore +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/README.md +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/hatch_build.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/pyproject.toml +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/__init__.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/cli/__init__.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/cli/app.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/cli/doctor.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/cli/tui.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/config.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/deliver.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/__init__.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/model.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/phases.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/prompts/__init__.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/prompts/decompose.md +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/prompts/execute.md +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/prompts/plan.md +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/prompts/scrutinize.md +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/prompts/validate.md +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/engine/store.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/errors.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/events.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/gh/__init__.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/gh/ops.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/ids.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/py.typed +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/__init__.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/cli.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/models.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/pair.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/parse.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/provision.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/sbx/sandbox.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/worker/__init__.py +0 -0
- {sbxloop-0.5.1 → sbxloop-0.5.3}/src/sbxloop/worker/client.py +0 -0
- {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.
|
|
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
|
|
Binary file
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 5,
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|