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.
Files changed (22) hide show
  1. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/PKG-INFO +1 -1
  2. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/cli.py +55 -14
  3. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/channel.py +16 -0
  4. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/runner.py +7 -6
  5. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli.egg-info/PKG-INFO +1 -1
  6. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/pyproject.toml +1 -1
  7. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/README.md +0 -0
  8. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/__init__.py +0 -0
  9. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/__init__.py +0 -0
  10. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/base.py +0 -0
  11. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/parsers/__init__.py +0 -0
  12. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/parsers/base.py +0 -0
  13. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/parsers/bash_agent.py +0 -0
  14. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/parsers/claude.py +0 -0
  15. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/_vendored/parsers/codex.py +0 -0
  16. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli/main.py +0 -0
  17. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli.egg-info/SOURCES.txt +0 -0
  18. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli.egg-info/dependency_links.txt +0 -0
  19. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli.egg-info/entry_points.txt +0 -0
  20. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli.egg-info/requires.txt +0 -0
  21. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/baserun_cli.egg-info/top_level.txt +0 -0
  22. {baserun_cli-0.1.0 → baserun_cli-0.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: baserun-cli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: BaseRun agent-side daemon (connects to nchan, spawns CLI agents, publishes run events)
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -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) or (
105
- self.spec.bin if os.path.exists(self.spec.bin) else self.spec.bin
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 = await asyncio.create_subprocess_exec(
111
- cli,
112
- *args,
113
- stdout=asyncio.subprocess.PIPE,
114
- stderr=asyncio.subprocess.PIPE,
115
- env=self._env(),
116
- cwd=self.spec.workdir,
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
- await self.channel.publish_event(run_id, payload)
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
- ok = await self.channel.publish_event(run_id, payload)
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 via HTTP (reliable)
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.publish_event(run_id, error_payload)
270
+ await self.channel.publish_event_reliably(run_id, error_payload)
270
271
  return True, "error", seq
271
272
 
272
273
  finally:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: baserun-cli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: BaseRun agent-side daemon (connects to nchan, spawns CLI agents, publishes run events)
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "baserun-cli"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "BaseRun agent-side daemon (connects to nchan, spawns CLI agents, publishes run events)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
File without changes
File without changes