sbxloop-worker 0.3.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.
Files changed (22) hide show
  1. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/PKG-INFO +1 -1
  2. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/pyproject.toml +1 -1
  3. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/__init__.py +1 -1
  4. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/backends/copilot.py +69 -5
  5. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/.gitignore +0 -0
  6. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/README.md +0 -0
  7. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/__main__.py +0 -0
  8. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/_json.py +0 -0
  9. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/backends/__init__.py +0 -0
  10. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/backends/echo.py +0 -0
  11. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/events.py +0 -0
  12. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/githubops.py +0 -0
  13. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/protocol.py +0 -0
  14. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/py.typed +0 -0
  15. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/src/sbxloop_worker/runner.py +0 -0
  16. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/tests/conftest.py +0 -0
  17. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/tests/test_githubops.py +0 -0
  18. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/tests/test_json_extract.py +0 -0
  19. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/tests/test_protocol.py +0 -0
  20. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/tests/test_runner_unit.py +0 -0
  21. {sbxloop_worker-0.3.0 → sbxloop_worker-0.4.0}/tests/test_worker_contract.py +0 -0
  22. {sbxloop_worker-0.3.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.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sbxloop-worker"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "In-sandbox worker runtime for sbxloop (protocol models, agent backends, job runner)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -5,6 +5,6 @@ models, the agent backends (GitHub Copilot SDK), and the job runner invoked
5
5
  via ``python -m sbxloop_worker``.
6
6
  """
7
7
 
8
- __version__ = "0.3.0"
8
+ __version__ = "0.4.0"
9
9
 
10
10
  __all__ = ["__version__"]
@@ -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=getattr(data, "tool_name", None) or getattr(data, "toolName", None),
97
- tool_call_id=getattr(data, "tool_call_id", None)
98
- or getattr(data, "toolCallId", None),
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=getattr(data, "tool_call_id", None)
104
- or getattr(data, "toolCallId", None),
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(
File without changes