Skip to content

Commit 2bec52d

Browse files
committed
Implement OpenClaw runtime config
1 parent 4538630 commit 2bec52d

1 file changed

Lines changed: 102 additions & 2 deletions

File tree

lagent/adapters/openclaw.py

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,20 @@
2525
2626
Usage::
2727
28+
# Set OPENAI_BASE_URL / OPENAI_API_KEY, or pass ``proxy=...`` and let
29+
# the adapter inject them into the OpenClaw subprocess.
2830
from lagent.adapters.openclaw import OpenClawAdapter
2931
30-
agent = OpenClawAdapter(thinking='medium', timeout=120)
32+
agent = OpenClawAdapter(model='gpt-4o-mini', thinking='medium', timeout=120)
3133
r1 = await agent("What is 2+2?")
3234
r2 = await agent("Now multiply by 3") # multi-turn via --session-id
3335
"""
3436

3537
import json
3638
import os
3739
import shlex
40+
import shutil
41+
from pathlib import Path
3842
from typing import List, Optional
3943

4044
from .cli_adapter import CLIAgentAdapter
@@ -44,12 +48,15 @@ class OpenClawAdapter(CLIAgentAdapter):
4448
"""Wraps the ``openclaw`` CLI as an :class:`AsyncExternalAgent`.
4549
4650
Args:
51+
model: Model id exposed by the configured OpenAI-compatible backend.
4752
thinking: Thinking level (``off`` / ``minimal`` / ``low`` /
4853
``medium`` / ``high`` / ``xhigh``).
4954
agent_id: OpenClaw agent id (``--agent``). Default: ``"main"``.
5055
json_output: Pass ``--json`` and parse the JSON envelope to
5156
extract ``sessionId`` (multi-turn) and the reply text.
5257
Default: True.
58+
openclaw_home: OpenClaw state/config directory. Default:
59+
``OPENCLAW_HOME`` / ``OPENCLAW_STATE_DIR`` / ``~/.openclaw``.
5360
nvm_dir: If set, wrap the spawn in ``bash -lc`` and source
5461
``$nvm_dir/nvm.sh`` before invoking ``openclaw``. Use this
5562
when the binary is provided by nvm and not on PATH.
@@ -66,6 +73,7 @@ def __init__(
6673
thinking: str = 'medium',
6774
agent_id: Optional[str] = 'main',
6875
json_output: bool = True,
76+
openclaw_home: Optional[str] = None,
6977
nvm_dir: Optional[str] = None,
7078
node_version: str = '22',
7179
binary: str = 'openclaw',
@@ -74,14 +82,34 @@ def __init__(
7482
kwargs.setdefault('name', 'openclaw')
7583
kwargs.setdefault('description', 'OpenClaw personal AI assistant')
7684
super().__init__(binary=binary, **kwargs)
85+
if not model:
86+
raise ValueError('OpenClawAdapter requires `model`.')
7787
self.model = model
7888
self.thinking = thinking
7989
self.agent_id = agent_id
8090
self.json_output = json_output
8191
self.nvm_dir = nvm_dir
8292
self.node_version = node_version
93+
self.provider = 'custom-openai'
94+
self.openclaw_home = Path(
95+
openclaw_home
96+
or self.env_vars.get('OPENCLAW_HOME')
97+
or self.env_vars.get('OPENCLAW_STATE_DIR')
98+
or os.environ.get('OPENCLAW_HOME')
99+
or os.environ.get('OPENCLAW_STATE_DIR')
100+
or Path.home() / '.openclaw'
101+
).expanduser()
102+
self.openclaw_config_path = Path(
103+
self.env_vars.get('OPENCLAW_CONFIG_PATH')
104+
or os.environ.get('OPENCLAW_CONFIG_PATH')
105+
or self.openclaw_home / 'openclaw.json'
106+
).expanduser()
107+
self.env_vars.setdefault('OPENCLAW_HOME', str(self.openclaw_home))
108+
self.env_vars.setdefault('OPENCLAW_STATE_DIR', str(self.openclaw_home))
109+
self.env_vars.setdefault('OPENCLAW_CONFIG_PATH', str(self.openclaw_config_path))
110+
self.env_vars.setdefault('NO_COLOR', '1')
83111
self._cli_session_id: Optional[str] = None
84-
self._write_openclaw_config()
112+
self._runtime_config_written = False
85113

86114
def setup(self) -> None:
87115
if self.nvm_dir:
@@ -95,6 +123,11 @@ def setup(self) -> None:
95123
return
96124
super().setup()
97125

126+
async def run_external_async(self, task: str, **kwargs) -> str:
127+
# 关键点:SessionClient 的端口在 forward() 里启动后才确定,因此配置必须运行前写。
128+
self._write_openclaw_config()
129+
return await super().run_external_async(task, **kwargs)
130+
98131
def _build_argv(self, task: str) -> List[str]:
99132
cli_args = ['agent', '--local', '--message', task, '--thinking', self.thinking]
100133
if self.json_output:
@@ -123,6 +156,73 @@ def reset_session(self) -> None:
123156
"""Forget the captured session id; the next call starts fresh."""
124157
self._cli_session_id = None
125158

159+
def _write_openclaw_config(self) -> None:
160+
env = self._build_env()
161+
base_url = (env.get('OPENAI_BASE_URL') or '').rstrip('/')
162+
api_key = env.get('OPENAI_API_KEY')
163+
if not base_url:
164+
raise RuntimeError(
165+
'OpenClawAdapter needs OPENAI_BASE_URL in subprocess env.'
166+
)
167+
if not api_key:
168+
raise RuntimeError(
169+
'OpenClawAdapter needs OPENAI_API_KEY in subprocess env.'
170+
)
171+
172+
agent_id = self.agent_id or 'main'
173+
model_ref = f'{self.provider}/{self.model}'
174+
workspace = self.working_dir or os.environ.get('TASK_WORKSPACE') or os.getcwd()
175+
agents = {
176+
'defaults': {
177+
'model': {'primary': model_ref},
178+
'workspace': workspace,
179+
}
180+
}
181+
if agent_id != 'main':
182+
agents['list'] = [
183+
{
184+
'id': agent_id,
185+
'model': {'primary': model_ref},
186+
'workspace': workspace,
187+
}
188+
]
189+
190+
config = {
191+
'models': {
192+
'mode': 'merge',
193+
'providers': {
194+
self.provider: {
195+
'baseUrl': base_url,
196+
'apiKey': '$OPENAI_API_KEY',
197+
'api': os.environ.get('OPENCLAW_PROVIDER_API', 'openai-completions'),
198+
'models': [
199+
{
200+
'id': self.model,
201+
'name': self.model,
202+
'reasoning': True,
203+
'input': ['text'],
204+
'contextWindow': int(os.environ.get('OPENCLAW_CONTEXT_WINDOW', '128000')),
205+
'maxTokens': int(os.environ.get('OPENCLAW_MAX_TOKENS', '16384')),
206+
}
207+
],
208+
}
209+
},
210+
},
211+
'agents': agents,
212+
}
213+
214+
self.openclaw_config_path.parent.mkdir(parents=True, exist_ok=True)
215+
self.openclaw_config_path.write_text(
216+
json.dumps(config, indent=2, ensure_ascii=False),
217+
encoding='utf-8',
218+
)
219+
220+
if not self._runtime_config_written:
221+
sessions_dir = self.openclaw_home / 'agents' / agent_id / 'sessions'
222+
shutil.rmtree(sessions_dir, ignore_errors=True)
223+
sessions_dir.mkdir(parents=True, exist_ok=True)
224+
self._runtime_config_written = True
225+
126226
def _default_parse(self, stdout: str, stderr: str) -> str:
127227
if not self.json_output:
128228
return stdout.strip()

0 commit comments

Comments
 (0)