From 9993fbfd2ed509e489bf78ca0e7b80f241e3d90a Mon Sep 17 00:00:00 2001 From: zhaopenghao Date: Tue, 23 Jun 2026 06:40:11 +0000 Subject: [PATCH 1/3] Enhance sandbox client error handling in _call_json function to include extra_info checks --- lagent/serving/sandbox/client_cli.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lagent/serving/sandbox/client_cli.py b/lagent/serving/sandbox/client_cli.py index 7b05055..47f253e 100644 --- a/lagent/serving/sandbox/client_cli.py +++ b/lagent/serving/sandbox/client_cli.py @@ -32,10 +32,14 @@ def _call_json(sock: str, payload: dict[str, Any]) -> dict[str, Any] | list: obj = json.loads(raw or "{}") except json.JSONDecodeError as exc: raise DaemonCallError(f"invalid daemon response: {exc}: {raw}") from exc - if isinstance(obj, dict) and obj.get("error"): - raise DaemonCallError(str(obj["error"])) if not isinstance(obj, (dict, list)): raise DaemonCallError(f"daemon response must be a JSON object or array, got {type(obj).__name__}") + if isinstance(obj, dict): + if obj.get("error"): + raise DaemonCallError(str(obj["error"])) + extra_info = obj.get("extra_info") + if isinstance(extra_info, dict) and extra_info.get("error"): + raise DaemonCallError(str(extra_info["error"])) return obj From 84e1ef0b250914ffb924b307ae87839ac1fb6d37 Mon Sep 17 00:00:00 2001 From: zhaopenghao Date: Wed, 24 Jun 2026 07:51:51 +0000 Subject: [PATCH 2/3] Add OpenClaw version detection and support for provider timeout in OpenClawAdapter for OpenClaw versions >= 2026.4.26. --- lagent/adapters/openclaw.py | 40 +++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/lagent/adapters/openclaw.py b/lagent/adapters/openclaw.py index a336512..9ad080a 100644 --- a/lagent/adapters/openclaw.py +++ b/lagent/adapters/openclaw.py @@ -36,10 +36,12 @@ import json import os +import re import shlex import shutil +import subprocess from pathlib import Path -from typing import List, Optional +from typing import List, Optional, Tuple from .cli_adapter import CLIAgentAdapter @@ -116,6 +118,36 @@ def __init__( self.env_vars.setdefault('NO_COLOR', '1') self._cli_session_id: Optional[str] = None self._runtime_config_written = False + self._openclaw_version: Optional[Tuple[int, int, int]] = None + + @staticmethod + def _parse_openclaw_version(text: str) -> Optional[Tuple[int, int, int]]: + match = re.search(r'OpenClaw\s+(\d+)\.(\d+)\.(\d+)', text) + if not match: + return None + return tuple(int(part) for part in match.groups()) + + def _detect_openclaw_version(self) -> Optional[Tuple[int, int, int]]: + if self._openclaw_version is not None: + return self._openclaw_version + try: + proc = subprocess.run( + [self.binary, '--version'], + capture_output=True, + text=True, + timeout=10, + check=False, + ) + text = (proc.stdout or proc.stderr or '').strip() + self._openclaw_version = self._parse_openclaw_version(text) + except (OSError, subprocess.SubprocessError): + self._openclaw_version = None + return self._openclaw_version + + def _supports_provider_timeout_seconds(self) -> bool: + """``models.providers.*.timeoutSeconds`` landed in 2026.4.26.""" + version = self._detect_openclaw_version() + return version is not None and version >= (2026, 4, 26) def setup(self) -> None: if self.nvm_dir: @@ -254,10 +286,10 @@ def _write_openclaw_config(self) -> None: } ], } - # OpenClaw aborts a model request when no response chunks arrive before the idle window. - # https://docs.openclaw.ai/concepts/agent-loop#timeouts + # Provider-scoped HTTP timeout is only valid on OpenClaw >= 2026.4.26. + # Agent run ceiling is handled separately via ``--timeout`` in _build_argv. provider_timeout = os.environ.get('OPENCLAW_PROVIDER_TIMEOUT_SECONDS', "1200") - if provider_timeout: + if provider_timeout and self._supports_provider_timeout_seconds(): provider_config['timeoutSeconds'] = int(provider_timeout) config = { From f08da0e3afc1a9d4468c2a0cf488425f5fb5579a Mon Sep 17 00:00:00 2001 From: zhaopenghao Date: Wed, 24 Jun 2026 09:01:38 +0000 Subject: [PATCH 3/3] Add JSON loading functionality to OpenClawAdapter for improved error handling and parsing from mixed stdout/stderr logs. --- lagent/adapters/openclaw.py | 46 ++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/lagent/adapters/openclaw.py b/lagent/adapters/openclaw.py index 9ad080a..fd5380b 100644 --- a/lagent/adapters/openclaw.py +++ b/lagent/adapters/openclaw.py @@ -139,6 +139,7 @@ def _detect_openclaw_version(self) -> Optional[Tuple[int, int, int]]: check=False, ) text = (proc.stdout or proc.stderr or '').strip() + print(f"openclaw version: {text}") self._openclaw_version = self._parse_openclaw_version(text) except (OSError, subprocess.SubprocessError): self._openclaw_version = None @@ -196,6 +197,32 @@ def reset_session(self) -> None: """Forget the captured session id; the next call starts fresh.""" self._cli_session_id = None + @staticmethod + def _load_openclaw_json(text: str) -> Optional[dict]: + """Load an OpenClaw JSON envelope from stdout or mixed stderr logs.""" + stripped = text.strip() + if not stripped: + return None + try: + data = json.loads(stripped) + return data if isinstance(data, dict) else None + except json.JSONDecodeError: + pass + + decoder = json.JSONDecoder() + fallback = None + for match in re.finditer(r'\{', stripped): + try: + data, _ = decoder.raw_decode(stripped[match.start():]) + except json.JSONDecodeError: + continue + if not isinstance(data, dict): + continue + if 'payloads' in data or 'meta' in data: + return data + fallback = data + return fallback + _IDENTITY_TEMPLATE_MARKER = 'Fill this in during your first conversation' _USER_TEMPLATE_MARKER = '_Learn about the person you' _MINIMAL_IDENTITY = """\ @@ -318,9 +345,14 @@ def _default_parse(self, stdout: str, stderr: str) -> str: if not self.json_output: return stdout.strip() - try: - data = json.loads(stdout.strip()) - except json.JSONDecodeError: + # 低版本 OpenClaw 在某些错误路径会 exit 0、stdout 为空,并把 JSON envelope + # 混在 stderr 日志后面;不能把这种情况静默解析成空字符串。 + data = self._load_openclaw_json(stdout) + parsed_from_stderr = False + if data is None and not stdout.strip(): + data = self._load_openclaw_json(stderr) + parsed_from_stderr = data is not None + if data is None: return stdout.strip() if not isinstance(data, dict): @@ -352,11 +384,15 @@ def _default_parse(self, stdout: str, stderr: str) -> str: ] joined = '\n'.join(p for p in parts if p) if joined: + if parsed_from_stderr and 'isError=true' in stderr: + raise RuntimeError(f'OpenClaw error: {joined}') return joined for key in ('reply', 'message', 'response', 'output', 'text', 'result', 'content'): value = data.get(key) if isinstance(value, str) and value: + if parsed_from_stderr and 'isError=true' in stderr: + raise RuntimeError(f'OpenClaw error: {value}') return value if isinstance(value, list): blocks = [ @@ -365,5 +401,9 @@ def _default_parse(self, stdout: str, stderr: str) -> str: ] joined = '\n'.join(b for b in blocks if b) if joined: + if parsed_from_stderr and 'isError=true' in stderr: + raise RuntimeError(f'OpenClaw error: {joined}') return joined + if parsed_from_stderr and stderr.strip(): + raise RuntimeError(f'OpenClaw error: {stderr.strip()[:2000]}') return json.dumps(data, ensure_ascii=False)