sbxloop-worker 0.2.0__tar.gz → 0.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.
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/PKG-INFO +1 -1
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/pyproject.toml +1 -1
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/__init__.py +1 -1
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/__main__.py +13 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/backends/copilot.py +84 -5
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/backends/echo.py +9 -1
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/protocol.py +4 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/.gitignore +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/README.md +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/_json.py +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/backends/__init__.py +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/events.py +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/githubops.py +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/py.typed +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/runner.py +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/tests/conftest.py +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/tests/test_githubops.py +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/tests/test_json_extract.py +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/tests/test_protocol.py +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/tests/test_runner_unit.py +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/tests/test_worker_contract.py +0 -0
- {sbxloop_worker-0.2.0 → sbxloop_worker-0.4.0}/tests/worker_harness.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sbxloop-worker
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: In-sandbox worker runtime for sbxloop (protocol models, agent backends, job runner)
|
|
5
5
|
Project-URL: Homepage, https://github.com/brettbergin/sbxloop
|
|
6
6
|
Project-URL: Repository, https://github.com/brettbergin/sbxloop
|
|
@@ -65,6 +65,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
65
65
|
run.add_argument("--result", required=True, type=Path)
|
|
66
66
|
run.add_argument("--heartbeat", type=float, default=15.0)
|
|
67
67
|
run.add_argument("--env-file", type=Path, default=DEFAULT_ENV_FILE)
|
|
68
|
+
run.add_argument("--cwd", type=Path, default=None)
|
|
68
69
|
args = parser.parse_args(argv)
|
|
69
70
|
|
|
70
71
|
apply_env_file(args.env_file)
|
|
@@ -76,6 +77,18 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
76
77
|
print(f"sbxloop_worker: invalid job file {args.job}: {exc}", file=sys.stderr)
|
|
77
78
|
return 64
|
|
78
79
|
|
|
80
|
+
if args.cwd is not None:
|
|
81
|
+
# Run the whole worker in the job's working directory so agent
|
|
82
|
+
# sessions (which inherit the process cwd) and shell commands both
|
|
83
|
+
# execute in the run workspace. Overwrite job.cwd with the resolved
|
|
84
|
+
# value so shell.check keeps its single code path.
|
|
85
|
+
try:
|
|
86
|
+
os.chdir(args.cwd)
|
|
87
|
+
except OSError as exc:
|
|
88
|
+
print(f"sbxloop_worker: cannot chdir to {args.cwd}: {exc}", file=sys.stderr)
|
|
89
|
+
return 64
|
|
90
|
+
job = job.model_copy(update={"cwd": str(Path.cwd())})
|
|
91
|
+
|
|
79
92
|
try:
|
|
80
93
|
JobRunner(
|
|
81
94
|
job,
|
|
@@ -17,6 +17,7 @@ from unit coverage accordingly.
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import asyncio
|
|
20
|
+
import json
|
|
20
21
|
import os
|
|
21
22
|
from typing import Any
|
|
22
23
|
|
|
@@ -52,6 +53,59 @@ def _auth_diagnostic() -> str:
|
|
|
52
53
|
)
|
|
53
54
|
|
|
54
55
|
|
|
56
|
+
# Transcript-facing clips: the event stream is telemetry, not the artifact
|
|
57
|
+
# channel, so payloads are bounded aggressively.
|
|
58
|
+
TOOL_ARGS_CLIP = 400
|
|
59
|
+
TOOL_OUTPUT_CLIP = 1_000
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _tool_args(arguments: Any) -> str | None:
|
|
63
|
+
"""The tool's arguments as one displayable string (best-effort).
|
|
64
|
+
|
|
65
|
+
``ToolExecutionStartData.arguments`` is typed ``Any`` by the SDK: shell
|
|
66
|
+
tools carry ``{"command": ...}``-shaped dicts, others arbitrary JSON.
|
|
67
|
+
Prefer the human-relevant fields, fall back to compact JSON.
|
|
68
|
+
"""
|
|
69
|
+
if arguments is None:
|
|
70
|
+
return None
|
|
71
|
+
if isinstance(arguments, dict):
|
|
72
|
+
for key in ("command", "cmd", "input", "query", "path"):
|
|
73
|
+
value = arguments.get(key)
|
|
74
|
+
if isinstance(value, str) and value.strip():
|
|
75
|
+
return value[:TOOL_ARGS_CLIP]
|
|
76
|
+
try:
|
|
77
|
+
return json.dumps(arguments, separators=(",", ":"))[:TOOL_ARGS_CLIP]
|
|
78
|
+
except (TypeError, ValueError):
|
|
79
|
+
return str(arguments)[:TOOL_ARGS_CLIP]
|
|
80
|
+
text = str(arguments)
|
|
81
|
+
return text[:TOOL_ARGS_CLIP] if text.strip() else None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _tool_exit_code(data: Any) -> int | None:
|
|
85
|
+
"""Shell exit code from a ToolExecutionComplete, when present.
|
|
86
|
+
|
|
87
|
+
Shell completions carry a ShellExit entry in ``result.contents``
|
|
88
|
+
(verified against github-copilot-sdk: ``exit_code: int``).
|
|
89
|
+
"""
|
|
90
|
+
result = getattr(data, "result", None)
|
|
91
|
+
for entry in getattr(result, "contents", None) or []:
|
|
92
|
+
code = getattr(entry, "exit_code", None)
|
|
93
|
+
if isinstance(code, int):
|
|
94
|
+
return code
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _tool_output(data: Any) -> str | None:
|
|
99
|
+
"""A bounded tail of the tool's output, for transcript display."""
|
|
100
|
+
result = getattr(data, "result", None)
|
|
101
|
+
content = getattr(result, "content", None)
|
|
102
|
+
if not content and result is not None:
|
|
103
|
+
content = getattr(result, "detailed_content", None)
|
|
104
|
+
if not isinstance(content, str) or not content.strip():
|
|
105
|
+
return None
|
|
106
|
+
return content[-TOOL_OUTPUT_CLIP:]
|
|
107
|
+
|
|
108
|
+
|
|
55
109
|
class CopilotBackend:
|
|
56
110
|
name = "copilot"
|
|
57
111
|
|
|
@@ -75,6 +129,9 @@ class CopilotBackend:
|
|
|
75
129
|
|
|
76
130
|
usage = Usage()
|
|
77
131
|
final_text: list[str] = []
|
|
132
|
+
# tool_call_id -> tool name, so completion events can name the tool
|
|
133
|
+
# (the SDK's Complete event carries only the call id).
|
|
134
|
+
tool_names: dict[str, str] = {}
|
|
78
135
|
|
|
79
136
|
def on_event(event: Any) -> None:
|
|
80
137
|
nonlocal usage
|
|
@@ -91,18 +148,25 @@ class CopilotBackend:
|
|
|
91
148
|
final_text.append(content)
|
|
92
149
|
emit(EventTypes.AGENT_MESSAGE, content=content)
|
|
93
150
|
elif type_name.startswith("ToolExecutionStart"):
|
|
151
|
+
tool = getattr(data, "tool_name", None) or getattr(data, "toolName", None)
|
|
152
|
+
call_id = getattr(data, "tool_call_id", None) or getattr(data, "toolCallId", None)
|
|
153
|
+
if tool and call_id:
|
|
154
|
+
tool_names[str(call_id)] = str(tool)
|
|
94
155
|
emit(
|
|
95
156
|
EventTypes.AGENT_TOOL_START,
|
|
96
|
-
tool=
|
|
97
|
-
tool_call_id=
|
|
98
|
-
|
|
157
|
+
tool=tool,
|
|
158
|
+
tool_call_id=call_id,
|
|
159
|
+
args=_tool_args(getattr(data, "arguments", None)),
|
|
99
160
|
)
|
|
100
161
|
elif type_name.startswith("ToolExecutionComplete"):
|
|
162
|
+
call_id = getattr(data, "tool_call_id", None) or getattr(data, "toolCallId", None)
|
|
101
163
|
emit(
|
|
102
164
|
EventTypes.AGENT_TOOL_END,
|
|
103
|
-
tool_call_id=
|
|
104
|
-
|
|
165
|
+
tool_call_id=call_id,
|
|
166
|
+
tool=tool_names.get(str(call_id)),
|
|
105
167
|
success=getattr(data, "success", None),
|
|
168
|
+
exit_code=_tool_exit_code(data),
|
|
169
|
+
output=_tool_output(data),
|
|
106
170
|
)
|
|
107
171
|
elif type_name == "AssistantUsageData":
|
|
108
172
|
sample = Usage(
|
|
@@ -142,6 +206,21 @@ class CopilotBackend:
|
|
|
142
206
|
kwargs["model"] = job.model
|
|
143
207
|
if job.system_message:
|
|
144
208
|
kwargs["system_message"] = {"mode": "append", "content": job.system_message}
|
|
209
|
+
if job.cwd:
|
|
210
|
+
# Belt-and-braces alongside the worker-level chdir; the kwarg
|
|
211
|
+
# name is unverified against the SDK (e2e-validated), so an
|
|
212
|
+
# unsupported signature must not fail the session.
|
|
213
|
+
kwargs["working_directory"] = job.cwd
|
|
214
|
+
try:
|
|
215
|
+
return await self._open(client, job, kwargs)
|
|
216
|
+
except TypeError:
|
|
217
|
+
if "working_directory" not in kwargs:
|
|
218
|
+
raise
|
|
219
|
+
del kwargs["working_directory"]
|
|
220
|
+
return await self._open(client, job, kwargs)
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
async def _open(client: Any, job: JobRequest, kwargs: dict[str, Any]) -> Any:
|
|
145
224
|
if job.resume_session_id:
|
|
146
225
|
return await client.resume_session(job.resume_session_id, **kwargs)
|
|
147
226
|
return await client.create_session(**kwargs)
|
|
@@ -7,7 +7,11 @@ multi-job engine tests can script an entire run.
|
|
|
7
7
|
|
|
8
8
|
Response object shape (all fields optional):
|
|
9
9
|
``{"text": str, "json": dict|list, "session_id": str, "sleep_s": float,
|
|
10
|
-
"events": [{"type": str, "data": {...}}], "fail": str
|
|
10
|
+
"events": [{"type": str, "data": {...}}], "fail": str,
|
|
11
|
+
"files": {"relative/path": "content"}}``
|
|
12
|
+
|
|
13
|
+
``files`` are written relative to the worker process cwd — modelling an
|
|
14
|
+
executor that produces artifacts in the run workspace.
|
|
11
15
|
"""
|
|
12
16
|
|
|
13
17
|
from __future__ import annotations
|
|
@@ -73,6 +77,10 @@ class EchoBackend:
|
|
|
73
77
|
raise RuntimeError(str(response["fail"]))
|
|
74
78
|
if sleep_s := float(response.get("sleep_s", 0)):
|
|
75
79
|
time.sleep(sleep_s)
|
|
80
|
+
for relative, content in response.get("files", {}).items():
|
|
81
|
+
target = Path.cwd() / relative
|
|
82
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
target.write_text(str(content))
|
|
76
84
|
for scripted_event in response.get("events", []):
|
|
77
85
|
emit(scripted_event["type"], **scripted_event.get("data", {}))
|
|
78
86
|
text = str(response.get("text", ""))
|
|
@@ -92,6 +92,10 @@ class JobRequest(ProtocolModel):
|
|
|
92
92
|
|
|
93
93
|
# kind == "shell.check"
|
|
94
94
|
argv: list[str] | None = None
|
|
95
|
+
|
|
96
|
+
# agent.session + shell.check: in-sandbox working directory. The worker
|
|
97
|
+
# process chdirs here (via --cwd) so agent sessions and shell commands
|
|
98
|
+
# run in the run's canonical workspace.
|
|
95
99
|
cwd: str | None = None
|
|
96
100
|
|
|
97
101
|
# kind == "github.op"
|
|
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
|