baserun-cli 0.1.0__tar.gz → 0.1.2__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.
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/PKG-INFO +1 -1
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/cli.py +55 -14
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/channel.py +16 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/runner.py +7 -6
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli.egg-info/PKG-INFO +1 -1
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/pyproject.toml +1 -1
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/README.md +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/__init__.py +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/__init__.py +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/base.py +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/parsers/__init__.py +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/parsers/base.py +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/parsers/bash_agent.py +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/parsers/claude.py +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/parsers/codex.py +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/main.py +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli.egg-info/SOURCES.txt +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli.egg-info/dependency_links.txt +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli.egg-info/entry_points.txt +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli.egg-info/requires.txt +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli.egg-info/top_level.txt +0 -0
- {baserun_cli-0.1.0 → baserun_cli-0.1.2}/setup.cfg +0 -0
|
@@ -24,6 +24,28 @@ from .base import (
|
|
|
24
24
|
from .parsers import extract_session_id, parse_line
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
_SUBPROCESS_STREAM_LIMIT = 64 * 1024 * 1024
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def _iter_lines(reader: asyncio.StreamReader) -> AsyncIterator[bytes]:
|
|
31
|
+
"""Yield newline-delimited output without StreamReader.readline limits."""
|
|
32
|
+
buf = bytearray()
|
|
33
|
+
while True:
|
|
34
|
+
chunk = await reader.read(64 * 1024)
|
|
35
|
+
if not chunk:
|
|
36
|
+
if buf:
|
|
37
|
+
yield bytes(buf)
|
|
38
|
+
return
|
|
39
|
+
buf.extend(chunk)
|
|
40
|
+
while True:
|
|
41
|
+
pos = buf.find(b"\n")
|
|
42
|
+
if pos < 0:
|
|
43
|
+
break
|
|
44
|
+
line = bytes(buf[:pos])
|
|
45
|
+
del buf[: pos + 1]
|
|
46
|
+
yield line
|
|
47
|
+
|
|
48
|
+
|
|
27
49
|
class CLIAgentConnector(AgentConnector):
|
|
28
50
|
"""The one connector implementation for all spec-compatible CLI agents."""
|
|
29
51
|
|
|
@@ -101,20 +123,37 @@ class CLIAgentConnector(AgentConnector):
|
|
|
101
123
|
) -> AsyncIterator[ConnectorEvent]:
|
|
102
124
|
import logging as _log
|
|
103
125
|
_dbg = _log.getLogger("baserun_cli._vendored.cli")
|
|
104
|
-
cli = shutil.which(self.spec.bin)
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
cli = shutil.which(self.spec.bin)
|
|
127
|
+
if not cli:
|
|
128
|
+
message = f"CLI not found: {self.spec.bin}. Please install it or configure cli_spec.bin with the correct executable path."
|
|
129
|
+
_dbg.error(message)
|
|
130
|
+
yield ConnectorEvent(ConnectorEventType.ERROR, {"message": message})
|
|
131
|
+
return
|
|
132
|
+
|
|
107
133
|
args = self._build_args(prompt, session_id, fork)
|
|
108
134
|
_dbg.info("spawning: %s %s (cwd=%s)", cli, " ".join(args[:6]) + ("..." if len(args) > 6 else ""), self.spec.workdir)
|
|
109
135
|
|
|
110
|
-
proc =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
136
|
+
proc = None
|
|
137
|
+
try:
|
|
138
|
+
proc = await asyncio.create_subprocess_exec(
|
|
139
|
+
cli,
|
|
140
|
+
*args,
|
|
141
|
+
stdout=asyncio.subprocess.PIPE,
|
|
142
|
+
stderr=asyncio.subprocess.PIPE,
|
|
143
|
+
env=self._env(),
|
|
144
|
+
cwd=self.spec.workdir,
|
|
145
|
+
limit=_SUBPROCESS_STREAM_LIMIT,
|
|
146
|
+
)
|
|
147
|
+
except FileNotFoundError:
|
|
148
|
+
message = f"CLI not found: {self.spec.bin}. Please install it or configure cli_spec.bin with the correct executable path."
|
|
149
|
+
_dbg.exception(message)
|
|
150
|
+
yield ConnectorEvent(ConnectorEventType.ERROR, {"message": message})
|
|
151
|
+
return
|
|
152
|
+
except Exception as e:
|
|
153
|
+
message = f"failed to start CLI {self.spec.bin}: {e}"
|
|
154
|
+
_dbg.exception(message)
|
|
155
|
+
yield ConnectorEvent(ConnectorEventType.ERROR, {"message": message})
|
|
156
|
+
return
|
|
118
157
|
_dbg.info("pid=%s started", proc.pid)
|
|
119
158
|
|
|
120
159
|
resolved_session_id: str | None = session_id
|
|
@@ -136,7 +175,7 @@ class CLIAgentConnector(AgentConnector):
|
|
|
136
175
|
|
|
137
176
|
try:
|
|
138
177
|
assert proc.stdout is not None
|
|
139
|
-
async for raw_line in proc.stdout:
|
|
178
|
+
async for raw_line in _iter_lines(proc.stdout):
|
|
140
179
|
line = raw_line.decode(errors="replace").strip()
|
|
141
180
|
if not line:
|
|
142
181
|
continue
|
|
@@ -196,7 +235,7 @@ class CLIAgentConnector(AgentConnector):
|
|
|
196
235
|
_dbg.exception("pid=%s _run exception", proc.pid)
|
|
197
236
|
yield ConnectorEvent(ConnectorEventType.ERROR, {"message": str(e)})
|
|
198
237
|
finally:
|
|
199
|
-
if proc.returncode is None:
|
|
238
|
+
if proc is not None and proc.returncode is None:
|
|
200
239
|
_dbg.warning("pid=%s still running, killing", proc.pid)
|
|
201
240
|
proc.kill()
|
|
202
241
|
await proc.wait()
|
|
@@ -263,6 +302,7 @@ class CLIAgentConnector(AgentConnector):
|
|
|
263
302
|
stderr=asyncio.subprocess.PIPE,
|
|
264
303
|
env=self._env(),
|
|
265
304
|
cwd=self.spec.workdir,
|
|
305
|
+
limit=_SUBPROCESS_STREAM_LIMIT,
|
|
266
306
|
)
|
|
267
307
|
try:
|
|
268
308
|
out_b, err_b = await asyncio.wait_for(proc.communicate(), timeout=60.0)
|
|
@@ -304,10 +344,11 @@ class CLIAgentConnector(AgentConnector):
|
|
|
304
344
|
stderr=asyncio.subprocess.PIPE,
|
|
305
345
|
env=self._env(),
|
|
306
346
|
cwd=self.spec.workdir,
|
|
347
|
+
limit=_SUBPROCESS_STREAM_LIMIT,
|
|
307
348
|
)
|
|
308
349
|
try:
|
|
309
350
|
assert proc2.stdout is not None
|
|
310
|
-
async for raw_line in proc2.stdout:
|
|
351
|
+
async for raw_line in _iter_lines(proc2.stdout):
|
|
311
352
|
line = raw_line.decode(errors="replace").strip()
|
|
312
353
|
if not line:
|
|
313
354
|
continue
|
|
@@ -133,6 +133,22 @@ class ChannelClient:
|
|
|
133
133
|
run_id, payload.get("data", {}).get("seq"), max_retries)
|
|
134
134
|
return False
|
|
135
135
|
|
|
136
|
+
async def publish_event_reliably(self, run_id: str, payload: dict[str, Any]) -> bool:
|
|
137
|
+
"""Publish a critical run event, falling back to HTTP if WS fails."""
|
|
138
|
+
if await self.publish_event(run_id, payload):
|
|
139
|
+
return True
|
|
140
|
+
try:
|
|
141
|
+
await self._http_publish(run_id, payload)
|
|
142
|
+
return True
|
|
143
|
+
except Exception as e:
|
|
144
|
+
log.error(
|
|
145
|
+
"http publish fallback failed for run %s seq=%s: %s",
|
|
146
|
+
run_id,
|
|
147
|
+
payload.get("data", {}).get("seq"),
|
|
148
|
+
e,
|
|
149
|
+
)
|
|
150
|
+
return False
|
|
151
|
+
|
|
136
152
|
async def _open_run_pub(self, run_id: str) -> Any:
|
|
137
153
|
"""Open a WS publisher connection for run:{run_id}."""
|
|
138
154
|
base = self.nchan_url
|
|
@@ -204,7 +204,10 @@ class TaskRunner:
|
|
|
204
204
|
log.debug("run %s event seq=%d kind=%s", run_id, seq, kind)
|
|
205
205
|
|
|
206
206
|
# publish
|
|
207
|
-
|
|
207
|
+
if is_terminal:
|
|
208
|
+
await self.channel.publish_event_reliably(run_id, payload)
|
|
209
|
+
else:
|
|
210
|
+
await self.channel.publish_event(run_id, payload)
|
|
208
211
|
|
|
209
212
|
if is_terminal:
|
|
210
213
|
saw_terminal = True
|
|
@@ -225,9 +228,7 @@ class TaskRunner:
|
|
|
225
228
|
}
|
|
226
229
|
log_file.write(json.dumps(payload["data"], ensure_ascii=False) + "\n")
|
|
227
230
|
log_file.flush()
|
|
228
|
-
|
|
229
|
-
if not ok:
|
|
230
|
-
failed_events.append(payload)
|
|
231
|
+
await self.channel.publish_event_reliably(run_id, payload)
|
|
231
232
|
log.info("run %s synthesized terminal (seq=%d)", run_id, seq)
|
|
232
233
|
|
|
233
234
|
# verify-and-replay:对比 nchan 实际状态,精确补发缺失事件
|
|
@@ -249,7 +250,7 @@ class TaskRunner:
|
|
|
249
250
|
|
|
250
251
|
except Exception as e:
|
|
251
252
|
log.exception("run %s failed at seq=%d", run_id, seq)
|
|
252
|
-
# publish error terminal
|
|
253
|
+
# publish error terminal reliably
|
|
253
254
|
seq += 1
|
|
254
255
|
error_payload = {
|
|
255
256
|
"type": "result",
|
|
@@ -266,7 +267,7 @@ class TaskRunner:
|
|
|
266
267
|
log_file.write(json.dumps(error_payload["data"], ensure_ascii=False) + "\n")
|
|
267
268
|
except Exception:
|
|
268
269
|
pass
|
|
269
|
-
await self.channel.
|
|
270
|
+
await self.channel.publish_event_reliably(run_id, error_payload)
|
|
270
271
|
return True, "error", seq
|
|
271
272
|
|
|
272
273
|
finally:
|
|
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
|