openreward 0.1.96.dev0__tar.gz → 0.1.96.dev2__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.
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/PKG-INFO +1 -1
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/environments/client.py +2 -2
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/hermes.py +93 -22
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/openclaw.py +148 -14
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward.egg-info/PKG-INFO +1 -1
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/pyproject.toml +1 -1
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_session_toolset.py +0 -114
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/README.md +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/__init__.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/_version.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/__init__.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/_session/__init__.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/_session/http.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/_session/ping.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/_session/session.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/environments/__init__.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/environments/types.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/__init__.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/background.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/rollout.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/__init__.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/ant.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/base.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/gdm.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/models.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/oai_completions.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/oai_responses.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/utils.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/sandboxes/__init__.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/sandboxes/client.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/sandboxes/secrets.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/sandboxes/types.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/cli.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/client.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/__init__.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/environment.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/reconnect.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/server.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/session.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/toolset.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/types.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/utils.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/http_client.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/log_utils.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/models.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/__init__.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/basic/Dockerfile +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/basic/__init__.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/basic/requirements.txt +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/basic/requirements.txt.tmpl +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/basic/server.py.tmpl +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/sandbox/Dockerfile +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/sandbox/__init__.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/sandbox/requirements.txt +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/sandbox/sandbox_env.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/sandbox/server.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/__init__.py +1 -1
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/claude_code.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/codex.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/excel.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/pdf.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/powerpoint.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/word.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward.egg-info/SOURCES.txt +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward.egg-info/dependency_links.txt +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward.egg-info/entry_points.txt +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward.egg-info/requires.txt +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward.egg-info/top_level.txt +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/setup.cfg +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_environment.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_errors.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_rollout_info.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_sanitise.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_session.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_tool_conversion.py +0 -0
- {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_toolsets.py +0 -0
|
@@ -12,8 +12,8 @@ from openreward.api._session.http import (
|
|
|
12
12
|
)
|
|
13
13
|
from openreward.api._session.session import BaseAsyncSession, SessionTerminatedError
|
|
14
14
|
|
|
15
|
-
BuiltinToolset = Literal["claude-code", "codex"
|
|
16
|
-
_VALID_BUILTIN_TOOLSETS = {"claude-code", "codex"
|
|
15
|
+
BuiltinToolset = Literal["claude-code", "codex"]
|
|
16
|
+
_VALID_BUILTIN_TOOLSETS = {"claude-code", "codex"}
|
|
17
17
|
from .types import (
|
|
18
18
|
ImageBlock,
|
|
19
19
|
JSONObject,
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
"""Hermes Agent session toolset.
|
|
2
2
|
|
|
3
|
-
Provides the
|
|
4
|
-
(``terminal``, ``read_file``, ``write_file``, ``patch``)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Tool names, parameter schemas, and descriptions match Hermes Agent's upstream
|
|
8
|
-
registry definitions (``nousresearch/hermes-agent``).
|
|
3
|
+
Provides the five built-in coding tools Hermes Agent exposes
|
|
4
|
+
(``terminal``, ``read_file``, ``write_file``, ``search_files``, ``patch``).
|
|
5
|
+
Tool names, parameter schemas, and descriptions match Hermes Agent's
|
|
6
|
+
upstream registry definitions (``nousresearch/hermes-agent``).
|
|
9
7
|
"""
|
|
10
8
|
from __future__ import annotations
|
|
11
9
|
|
|
@@ -20,8 +18,6 @@ from openreward.environments.toolset import Toolset
|
|
|
20
18
|
from openreward.environments.types import TextBlock, ToolOutput
|
|
21
19
|
|
|
22
20
|
|
|
23
|
-
# ── Sandbox text helpers (inlined; same as claude_code.py) ──
|
|
24
|
-
|
|
25
21
|
async def _download_text(sandbox: Any, path: str) -> str:
|
|
26
22
|
data = await sandbox.download(path)
|
|
27
23
|
return data.decode("utf-8")
|
|
@@ -57,6 +53,17 @@ class WriteFileParams(BaseModel, extra="forbid"):
|
|
|
57
53
|
content: str
|
|
58
54
|
|
|
59
55
|
|
|
56
|
+
class SearchFilesParams(BaseModel, extra="forbid"):
|
|
57
|
+
pattern: str
|
|
58
|
+
target: str = "content"
|
|
59
|
+
path: str = "."
|
|
60
|
+
file_glob: Optional[str] = None
|
|
61
|
+
limit: int = 50
|
|
62
|
+
offset: int = 0
|
|
63
|
+
output_mode: str = "content"
|
|
64
|
+
context: int = 0
|
|
65
|
+
|
|
66
|
+
|
|
60
67
|
class PatchParams(BaseModel, extra="forbid"):
|
|
61
68
|
mode: str = "replace"
|
|
62
69
|
path: Optional[str] = None
|
|
@@ -66,13 +73,14 @@ class PatchParams(BaseModel, extra="forbid"):
|
|
|
66
73
|
patch: Optional[str] = None
|
|
67
74
|
|
|
68
75
|
|
|
69
|
-
# ── Tool descriptions (matching Hermes registry
|
|
76
|
+
# ── Tool descriptions (matching Hermes upstream registry) ──
|
|
70
77
|
|
|
71
78
|
TERMINAL_DESCRIPTION = """\
|
|
72
79
|
Execute shell commands on a Linux environment. Filesystem usually persists between calls.
|
|
73
80
|
|
|
74
81
|
Do NOT use cat/head/tail to read files — use read_file instead.
|
|
75
82
|
Do NOT use grep/rg/find to search — use search_files instead.
|
|
83
|
+
Do NOT use ls to list directories — use search_files(target='files') instead.
|
|
76
84
|
Do NOT use sed/awk to edit files — use patch instead.
|
|
77
85
|
Do NOT use echo/cat heredoc to create files — use write_file instead.
|
|
78
86
|
Reserve terminal for: builds, installs, git, processes, scripts, network, package managers, and anything that needs a shell.
|
|
@@ -89,6 +97,17 @@ Write content to a file, completely replacing existing content. Use this instead
|
|
|
89
97
|
in terminal. Creates parent directories automatically. OVERWRITES the entire file — use 'patch' for \
|
|
90
98
|
targeted edits."""
|
|
91
99
|
|
|
100
|
+
SEARCH_FILES_DESCRIPTION = """\
|
|
101
|
+
Search file contents or find files by name using regex/glob patterns. Use this instead of \
|
|
102
|
+
grep/rg/find/ls in terminal.
|
|
103
|
+
|
|
104
|
+
target='content' (default): search inside file contents with regex. Returns matching lines with \
|
|
105
|
+
line numbers and optional context.
|
|
106
|
+
target='files': search for files by name/glob pattern. Returns matching file paths.
|
|
107
|
+
|
|
108
|
+
Use file_glob to filter which files to search (e.g., '*.py'). Use output_mode to control \
|
|
109
|
+
output format: 'content' (matching lines), 'files_only' (file paths), 'count' (match counts)."""
|
|
110
|
+
|
|
92
111
|
PATCH_DESCRIPTION = """\
|
|
93
112
|
Targeted find-and-replace edits in files. Use this instead of sed/awk in terminal.
|
|
94
113
|
|
|
@@ -102,10 +121,12 @@ Include enough surrounding context to ensure uniqueness."""
|
|
|
102
121
|
# ── Toolset ──
|
|
103
122
|
|
|
104
123
|
class HermesToolset(Toolset):
|
|
105
|
-
"""Session toolset exposing the Hermes Agent
|
|
124
|
+
"""Session toolset exposing the Hermes Agent five-tool coding surface.
|
|
106
125
|
|
|
107
126
|
The toolset is bound to a session by passing it to ``env.session(...)``::
|
|
108
127
|
|
|
128
|
+
from openreward.toolsets import HermesToolset
|
|
129
|
+
|
|
109
130
|
with env.session(task=task, toolset="hermes") as session:
|
|
110
131
|
session.call_tool("terminal", {"command": "ls"})
|
|
111
132
|
|
|
@@ -139,15 +160,11 @@ class HermesToolset(Toolset):
|
|
|
139
160
|
content = await _download_text(self.sandbox, params.path)
|
|
140
161
|
lines = content.splitlines()
|
|
141
162
|
|
|
142
|
-
# Apply offset (1-indexed) and limit
|
|
143
163
|
start = max(0, params.offset - 1)
|
|
144
164
|
end = start + params.limit
|
|
145
165
|
selected_lines = lines[start:end]
|
|
146
166
|
|
|
147
|
-
|
|
148
|
-
output_lines = []
|
|
149
|
-
for i, line in enumerate(selected_lines, start=start + 1):
|
|
150
|
-
output_lines.append(f"{i}|{line}")
|
|
167
|
+
output_lines = [f"{i}|{line}" for i, line in enumerate(selected_lines, start=start + 1)]
|
|
151
168
|
output = "\n".join(output_lines)
|
|
152
169
|
|
|
153
170
|
return ToolOutput(
|
|
@@ -188,6 +205,66 @@ class HermesToolset(Toolset):
|
|
|
188
205
|
finished=False,
|
|
189
206
|
)
|
|
190
207
|
|
|
208
|
+
@tool
|
|
209
|
+
async def search_files(self, params: SearchFilesParams) -> ToolOutput:
|
|
210
|
+
try:
|
|
211
|
+
if params.target == "files":
|
|
212
|
+
cmd = f"find {params.path} -type f -name '{params.pattern}'"
|
|
213
|
+
output, code = await self.sandbox.run(cmd)
|
|
214
|
+
if code != 0:
|
|
215
|
+
return ToolOutput(
|
|
216
|
+
metadata={"error": output, "exit_code": code},
|
|
217
|
+
blocks=[TextBlock(text=f"search_files failed (exit {code}):\n{output}")],
|
|
218
|
+
finished=False,
|
|
219
|
+
)
|
|
220
|
+
lines = [l for l in output.splitlines() if l.strip()]
|
|
221
|
+
lines = lines[params.offset:params.offset + params.limit]
|
|
222
|
+
result = "\n".join(lines)
|
|
223
|
+
return ToolOutput(
|
|
224
|
+
metadata={"output": result, "exit_code": 0},
|
|
225
|
+
blocks=[TextBlock(text=result if result else "No files found.")],
|
|
226
|
+
reward=0.0,
|
|
227
|
+
finished=False,
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
glob_flag = f" --include='{params.file_glob}'" if params.file_glob else ""
|
|
231
|
+
context_flag = f" -C {params.context}" if params.context > 0 else ""
|
|
232
|
+
|
|
233
|
+
if params.output_mode == "files_only":
|
|
234
|
+
mode_flag = " -l"
|
|
235
|
+
elif params.output_mode == "count":
|
|
236
|
+
mode_flag = " -c"
|
|
237
|
+
else:
|
|
238
|
+
mode_flag = " -n"
|
|
239
|
+
|
|
240
|
+
cmd = f"grep -r{mode_flag}{context_flag}{glob_flag} '{params.pattern}' {params.path}"
|
|
241
|
+
output, code = await self.sandbox.run(cmd)
|
|
242
|
+
|
|
243
|
+
# grep returns 1 for no matches — not an error
|
|
244
|
+
if code > 1:
|
|
245
|
+
return ToolOutput(
|
|
246
|
+
metadata={"error": output, "exit_code": code},
|
|
247
|
+
blocks=[TextBlock(text=f"search_files failed (exit {code}):\n{output}")],
|
|
248
|
+
finished=False,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
lines = output.splitlines()
|
|
252
|
+
lines = lines[params.offset:params.offset + params.limit]
|
|
253
|
+
result = "\n".join(lines)
|
|
254
|
+
|
|
255
|
+
return ToolOutput(
|
|
256
|
+
metadata={"output": result, "exit_code": 0},
|
|
257
|
+
blocks=[TextBlock(text=result if result else "No matches found.")],
|
|
258
|
+
reward=0.0,
|
|
259
|
+
finished=False,
|
|
260
|
+
)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
return ToolOutput(
|
|
263
|
+
metadata={"error": str(e)},
|
|
264
|
+
blocks=[TextBlock(text=f"Error searching files: {str(e)}")],
|
|
265
|
+
finished=False,
|
|
266
|
+
)
|
|
267
|
+
|
|
191
268
|
@tool
|
|
192
269
|
async def patch(self, params: PatchParams) -> ToolOutput:
|
|
193
270
|
if params.mode == "replace":
|
|
@@ -202,7 +279,6 @@ class HermesToolset(Toolset):
|
|
|
202
279
|
)
|
|
203
280
|
|
|
204
281
|
async def _patch_replace(self, params: PatchParams) -> ToolOutput:
|
|
205
|
-
"""Replace mode: find a unique string and replace it."""
|
|
206
282
|
try:
|
|
207
283
|
if not params.path or params.old_string is None or params.new_string is None:
|
|
208
284
|
return ToolOutput(
|
|
@@ -248,7 +324,6 @@ class HermesToolset(Toolset):
|
|
|
248
324
|
)
|
|
249
325
|
|
|
250
326
|
async def _patch_v4a(self, params: PatchParams) -> ToolOutput:
|
|
251
|
-
"""Patch mode: apply V4A multi-file patch content."""
|
|
252
327
|
try:
|
|
253
328
|
if not params.patch:
|
|
254
329
|
return ToolOutput(
|
|
@@ -257,14 +332,11 @@ class HermesToolset(Toolset):
|
|
|
257
332
|
finished=False,
|
|
258
333
|
)
|
|
259
334
|
|
|
260
|
-
# Upload patch content to a temp file and apply via patch command
|
|
261
335
|
patch_tmp = "/tmp/_hermes_patch.diff"
|
|
262
336
|
await _upload_text(self.sandbox, patch_tmp, params.patch, ensure_trailing_newline=True)
|
|
263
337
|
|
|
264
|
-
# Try applying as a unified diff first
|
|
265
338
|
output, code = await self.sandbox.run(f"patch -p1 < {patch_tmp}")
|
|
266
339
|
if code != 0:
|
|
267
|
-
# Clean up and report
|
|
268
340
|
await self.sandbox.run(f"rm -f {patch_tmp}")
|
|
269
341
|
return ToolOutput(
|
|
270
342
|
metadata={"error": output, "exit_code": code},
|
|
@@ -287,9 +359,8 @@ class HermesToolset(Toolset):
|
|
|
287
359
|
)
|
|
288
360
|
|
|
289
361
|
|
|
290
|
-
# Assign descriptions onto each tool method's __doc__ so the framework's
|
|
291
|
-
# introspection picks them up.
|
|
292
362
|
HermesToolset.terminal.__doc__ = TERMINAL_DESCRIPTION
|
|
293
363
|
HermesToolset.read_file.__doc__ = READ_FILE_DESCRIPTION
|
|
294
364
|
HermesToolset.write_file.__doc__ = WRITE_FILE_DESCRIPTION
|
|
365
|
+
HermesToolset.search_files.__doc__ = SEARCH_FILES_DESCRIPTION
|
|
295
366
|
HermesToolset.patch.__doc__ = PATCH_DESCRIPTION
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
"""OpenClaw session toolset.
|
|
2
2
|
|
|
3
|
-
Provides the
|
|
4
|
-
(``exec``, ``read``, ``write``, ``edit``)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Tool names, parameter schemas, and descriptions match OpenClaw's upstream
|
|
8
|
-
TypeBox definitions (``@mariozechner/pi-coding-agent``).
|
|
3
|
+
Provides the six built-in coding tools OpenClaw exposes
|
|
4
|
+
(``exec``, ``process``, ``read``, ``write``, ``edit``, ``apply_patch``).
|
|
5
|
+
Tool names, parameter schemas, and descriptions match OpenClaw's
|
|
6
|
+
upstream definitions.
|
|
9
7
|
"""
|
|
10
8
|
from __future__ import annotations
|
|
11
9
|
|
|
@@ -13,15 +11,13 @@ import base64
|
|
|
13
11
|
import os
|
|
14
12
|
from typing import Any, List, Optional
|
|
15
13
|
|
|
16
|
-
from pydantic import BaseModel
|
|
14
|
+
from pydantic import BaseModel
|
|
17
15
|
|
|
18
16
|
from openreward.environments.environment import tool
|
|
19
17
|
from openreward.environments.toolset import Toolset
|
|
20
18
|
from openreward.environments.types import TextBlock, ToolOutput
|
|
21
19
|
|
|
22
20
|
|
|
23
|
-
# ── Sandbox text helpers (inlined; same as claude_code.py) ──
|
|
24
|
-
|
|
25
21
|
async def _download_text(sandbox: Any, path: str) -> str:
|
|
26
22
|
data = await sandbox.download(path)
|
|
27
23
|
return data.decode("utf-8")
|
|
@@ -67,7 +63,20 @@ class EditParams(BaseModel, extra="forbid"):
|
|
|
67
63
|
edits: List[EditItem]
|
|
68
64
|
|
|
69
65
|
|
|
70
|
-
|
|
66
|
+
class ApplyPatchParams(BaseModel, extra="forbid"):
|
|
67
|
+
input: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ProcessParams(BaseModel, extra="forbid"):
|
|
71
|
+
action: str
|
|
72
|
+
sessionId: Optional[str] = None
|
|
73
|
+
data: Optional[str] = None
|
|
74
|
+
eof: Optional[bool] = None
|
|
75
|
+
offset: Optional[int] = None
|
|
76
|
+
limit: Optional[int] = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── Tool descriptions (matching OpenClaw upstream) ──
|
|
71
80
|
|
|
72
81
|
EXEC_DESCRIPTION = """\
|
|
73
82
|
Execute a shell command and return its output and exit code. \
|
|
@@ -90,14 +99,36 @@ Apply one or more targeted text replacements to a file. \
|
|
|
90
99
|
Each edit specifies an oldText to find and a newText to replace it with. \
|
|
91
100
|
oldText must be unique in the file for each edit."""
|
|
92
101
|
|
|
102
|
+
PROCESS_DESCRIPTION = """\
|
|
103
|
+
Manage background processes. Use exec with background=true to start a process, \
|
|
104
|
+
then use this tool to interact with it.
|
|
105
|
+
|
|
106
|
+
Actions:
|
|
107
|
+
- list: show running and finished background sessions
|
|
108
|
+
- poll: check for new output from a session (requires sessionId)
|
|
109
|
+
- log: read session output with optional offset/limit pagination (requires sessionId)
|
|
110
|
+
- write: send data to a session's stdin (requires sessionId and data)
|
|
111
|
+
- kill: terminate a background session (requires sessionId)
|
|
112
|
+
- remove: kill if running, clear if finished (requires sessionId)"""
|
|
113
|
+
|
|
114
|
+
APPLY_PATCH_DESCRIPTION = """\
|
|
115
|
+
Apply file modifications using a structured patch format, designed for multiple file \
|
|
116
|
+
or multi-hunk edits where individual edit calls would be fragile.
|
|
117
|
+
|
|
118
|
+
The input must include '*** Begin Patch' and '*** End Patch' markers. Supported \
|
|
119
|
+
operations: '*** Add File:', '*** Update File:' (with optional '*** Move to:'), \
|
|
120
|
+
'*** Delete File:', and '*** End of File' for EOF-only insertions."""
|
|
121
|
+
|
|
93
122
|
|
|
94
123
|
# ── Toolset ──
|
|
95
124
|
|
|
96
125
|
class OpenClawToolset(Toolset):
|
|
97
|
-
"""Session toolset exposing the OpenClaw
|
|
126
|
+
"""Session toolset exposing the OpenClaw six-tool coding surface.
|
|
98
127
|
|
|
99
128
|
The toolset is bound to a session by passing it to ``env.session(...)``::
|
|
100
129
|
|
|
130
|
+
from openreward.toolsets import OpenClawToolset
|
|
131
|
+
|
|
101
132
|
with env.session(task=task, toolset="openclaw") as session:
|
|
102
133
|
session.call_tool("exec", {"command": "ls"})
|
|
103
134
|
|
|
@@ -125,6 +156,80 @@ class OpenClawToolset(Toolset):
|
|
|
125
156
|
finished=False,
|
|
126
157
|
)
|
|
127
158
|
|
|
159
|
+
@tool
|
|
160
|
+
async def process(self, params: ProcessParams) -> ToolOutput:
|
|
161
|
+
try:
|
|
162
|
+
action = params.action
|
|
163
|
+
sid = params.sessionId or ""
|
|
164
|
+
|
|
165
|
+
if action == "list":
|
|
166
|
+
output, code = await self.sandbox.run("ps aux --no-headers 2>/dev/null || ps aux")
|
|
167
|
+
return ToolOutput(
|
|
168
|
+
blocks=[TextBlock(text=output if output.strip() else "No background processes.")],
|
|
169
|
+
metadata={"output": output, "exit_code": code},
|
|
170
|
+
reward=0.0,
|
|
171
|
+
finished=False,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if not sid:
|
|
175
|
+
return ToolOutput(
|
|
176
|
+
metadata={"error": "sessionId is required for this action"},
|
|
177
|
+
blocks=[TextBlock(text=f"Error: sessionId is required for action '{action}'.")],
|
|
178
|
+
finished=False,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if action in ("poll", "log"):
|
|
182
|
+
tail_n = params.limit or 200
|
|
183
|
+
cmd = f"cat /tmp/_oc_proc_{sid}.log 2>/dev/null || echo 'No output available for session {sid}'"
|
|
184
|
+
if params.offset is not None:
|
|
185
|
+
cmd = f"tail -n +{params.offset} /tmp/_oc_proc_{sid}.log 2>/dev/null | head -n {tail_n}"
|
|
186
|
+
elif params.limit:
|
|
187
|
+
cmd = f"tail -n {tail_n} /tmp/_oc_proc_{sid}.log 2>/dev/null"
|
|
188
|
+
output, code = await self.sandbox.run(cmd)
|
|
189
|
+
return ToolOutput(
|
|
190
|
+
blocks=[TextBlock(text=output)],
|
|
191
|
+
metadata={"output": output, "exit_code": code},
|
|
192
|
+
reward=0.0,
|
|
193
|
+
finished=False,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if action == "write":
|
|
197
|
+
data = params.data or ""
|
|
198
|
+
output, code = await self.sandbox.run(
|
|
199
|
+
f"echo '{data}' >> /tmp/_oc_proc_{sid}.stdin 2>/dev/null"
|
|
200
|
+
)
|
|
201
|
+
return ToolOutput(
|
|
202
|
+
blocks=[TextBlock(text=f"Sent data to session {sid}")],
|
|
203
|
+
metadata={"output": output, "exit_code": code},
|
|
204
|
+
reward=0.0,
|
|
205
|
+
finished=False,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if action in ("kill", "remove"):
|
|
209
|
+
output, code = await self.sandbox.run(
|
|
210
|
+
f"kill $(cat /tmp/_oc_proc_{sid}.pid 2>/dev/null) 2>/dev/null; "
|
|
211
|
+
f"rm -f /tmp/_oc_proc_{sid}.pid /tmp/_oc_proc_{sid}.log /tmp/_oc_proc_{sid}.stdin"
|
|
212
|
+
)
|
|
213
|
+
return ToolOutput(
|
|
214
|
+
blocks=[TextBlock(text=f"Session {sid} terminated and cleaned up.")],
|
|
215
|
+
metadata={"output": output, "exit_code": code},
|
|
216
|
+
reward=0.0,
|
|
217
|
+
finished=False,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return ToolOutput(
|
|
221
|
+
metadata={"error": f"Unknown action: {action}"},
|
|
222
|
+
blocks=[TextBlock(text=f"Error: unknown process action '{action}'. "
|
|
223
|
+
"Use list, poll, log, write, kill, or remove.")],
|
|
224
|
+
finished=False,
|
|
225
|
+
)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
return ToolOutput(
|
|
228
|
+
metadata={"error": str(e)},
|
|
229
|
+
blocks=[TextBlock(text=f"Error managing process: {str(e)}")],
|
|
230
|
+
finished=False,
|
|
231
|
+
)
|
|
232
|
+
|
|
128
233
|
@tool
|
|
129
234
|
async def read(self, params: ReadParams) -> ToolOutput:
|
|
130
235
|
try:
|
|
@@ -132,7 +237,7 @@ class OpenClawToolset(Toolset):
|
|
|
132
237
|
lines = content.splitlines()
|
|
133
238
|
|
|
134
239
|
if params.offset is not None or params.limit is not None:
|
|
135
|
-
start = (params.offset or 1) - 1
|
|
240
|
+
start = (params.offset or 1) - 1
|
|
136
241
|
if params.limit is not None:
|
|
137
242
|
lines = lines[start:start + params.limit]
|
|
138
243
|
else:
|
|
@@ -213,10 +318,39 @@ class OpenClawToolset(Toolset):
|
|
|
213
318
|
finished=False,
|
|
214
319
|
)
|
|
215
320
|
|
|
321
|
+
@tool
|
|
322
|
+
async def apply_patch(self, params: ApplyPatchParams) -> ToolOutput:
|
|
323
|
+
try:
|
|
324
|
+
patch_tmp = "/tmp/_openclaw_patch.diff"
|
|
325
|
+
await _upload_text(self.sandbox, patch_tmp, params.input, ensure_trailing_newline=True)
|
|
326
|
+
|
|
327
|
+
output, code = await self.sandbox.run(f"patch -p1 < {patch_tmp}")
|
|
328
|
+
if code != 0:
|
|
329
|
+
await self.sandbox.run(f"rm -f {patch_tmp}")
|
|
330
|
+
return ToolOutput(
|
|
331
|
+
metadata={"error": output, "exit_code": code},
|
|
332
|
+
blocks=[TextBlock(text=f"apply_patch failed (exit {code}):\n{output}")],
|
|
333
|
+
finished=False,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
await self.sandbox.run(f"rm -f {patch_tmp}")
|
|
337
|
+
return ToolOutput(
|
|
338
|
+
metadata={"output": output, "exit_code": 0},
|
|
339
|
+
blocks=[TextBlock(text=f"Successfully applied patch:\n{output}")],
|
|
340
|
+
reward=0.0,
|
|
341
|
+
finished=False,
|
|
342
|
+
)
|
|
343
|
+
except Exception as e:
|
|
344
|
+
return ToolOutput(
|
|
345
|
+
metadata={"error": str(e)},
|
|
346
|
+
blocks=[TextBlock(text=f"Error applying patch: {str(e)}")],
|
|
347
|
+
finished=False,
|
|
348
|
+
)
|
|
349
|
+
|
|
216
350
|
|
|
217
|
-
# Assign descriptions onto each tool method's __doc__ so the framework's
|
|
218
|
-
# introspection picks them up.
|
|
219
351
|
OpenClawToolset.exec.__doc__ = EXEC_DESCRIPTION
|
|
352
|
+
OpenClawToolset.process.__doc__ = PROCESS_DESCRIPTION
|
|
220
353
|
OpenClawToolset.read.__doc__ = READ_DESCRIPTION
|
|
221
354
|
OpenClawToolset.write.__doc__ = WRITE_DESCRIPTION
|
|
222
355
|
OpenClawToolset.edit.__doc__ = EDIT_DESCRIPTION
|
|
356
|
+
OpenClawToolset.apply_patch.__doc__ = APPLY_PATCH_DESCRIPTION
|
|
@@ -283,117 +283,3 @@ async def test_session_toolset_warns_on_shadow(monkeypatch):
|
|
|
283
283
|
call_warnings = [e for e in captured if e[0] == "session_toolset_shadows_env_tool"]
|
|
284
284
|
assert len(call_warnings) == 1
|
|
285
285
|
assert call_warnings[0][1]["tool"] == "bash"
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
# ── OpenClaw toolset ──
|
|
289
|
-
|
|
290
|
-
OPENCLAW_TOOLS = {"exec", "read", "write", "edit"}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
@pytest.mark.asyncio
|
|
294
|
-
async def test_openclaw_toolset_tools(client: AsyncOpenReward, server: str):
|
|
295
|
-
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
296
|
-
tasks = await env.list_tasks(split="train")
|
|
297
|
-
async with env.session(tasks[0], toolset="openclaw") as session:
|
|
298
|
-
tools = await session.list_tools()
|
|
299
|
-
toolset_names = {t.name for t in tools}
|
|
300
|
-
for name in OPENCLAW_TOOLS:
|
|
301
|
-
assert name in toolset_names, f"missing tool {name}"
|
|
302
|
-
# Env's submit tool is preserved.
|
|
303
|
-
assert "submit" in toolset_names
|
|
304
|
-
# exec description matches OpenClaw style.
|
|
305
|
-
exec_spec = next(t for t in tools if t.name == "exec")
|
|
306
|
-
assert exec_spec.description.startswith("Execute a shell command")
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
@pytest.mark.asyncio
|
|
310
|
-
async def test_openclaw_exec_routes_to_sandbox(client: AsyncOpenReward, server: str):
|
|
311
|
-
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
312
|
-
tasks = await env.list_tasks(split="train")
|
|
313
|
-
async with env.session(tasks[0], toolset="openclaw") as session:
|
|
314
|
-
result = await session.call_tool("exec", {"command": "echo hi"})
|
|
315
|
-
assert "ran: echo hi" in result.blocks[0].text
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
@pytest.mark.asyncio
|
|
319
|
-
async def test_openclaw_write_then_read_roundtrip(client: AsyncOpenReward, server: str):
|
|
320
|
-
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
321
|
-
tasks = await env.list_tasks(split="train")
|
|
322
|
-
async with env.session(tasks[0], toolset="openclaw") as session:
|
|
323
|
-
await session.call_tool("write", {"path": "/tmp/oc.txt", "content": "hello openclaw"})
|
|
324
|
-
result = await session.call_tool("read", {"path": "/tmp/oc.txt"})
|
|
325
|
-
assert "hello openclaw" in result.blocks[0].text
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
@pytest.mark.asyncio
|
|
329
|
-
async def test_openclaw_edit_with_edits_array(client: AsyncOpenReward, server: str):
|
|
330
|
-
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
331
|
-
tasks = await env.list_tasks(split="train")
|
|
332
|
-
async with env.session(tasks[0], toolset="openclaw") as session:
|
|
333
|
-
await session.call_tool("write", {"path": "/tmp/oc_edit.txt", "content": "foo bar baz"})
|
|
334
|
-
result = await session.call_tool("edit", {
|
|
335
|
-
"path": "/tmp/oc_edit.txt",
|
|
336
|
-
"edits": [{"oldText": "bar", "newText": "qux"}],
|
|
337
|
-
})
|
|
338
|
-
assert "Successfully edited" in result.blocks[0].text
|
|
339
|
-
read_result = await session.call_tool("read", {"path": "/tmp/oc_edit.txt"})
|
|
340
|
-
assert "qux" in read_result.blocks[0].text
|
|
341
|
-
assert "bar" not in read_result.blocks[0].text
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
# ── Hermes toolset ──
|
|
345
|
-
|
|
346
|
-
HERMES_TOOLS = {"terminal", "read_file", "write_file", "patch"}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
@pytest.mark.asyncio
|
|
350
|
-
async def test_hermes_toolset_tools(client: AsyncOpenReward, server: str):
|
|
351
|
-
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
352
|
-
tasks = await env.list_tasks(split="train")
|
|
353
|
-
async with env.session(tasks[0], toolset="hermes") as session:
|
|
354
|
-
tools = await session.list_tools()
|
|
355
|
-
toolset_names = {t.name for t in tools}
|
|
356
|
-
for name in HERMES_TOOLS:
|
|
357
|
-
assert name in toolset_names, f"missing tool {name}"
|
|
358
|
-
assert "submit" in toolset_names
|
|
359
|
-
terminal_spec = next(t for t in tools if t.name == "terminal")
|
|
360
|
-
assert terminal_spec.description.startswith("Execute shell commands")
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
@pytest.mark.asyncio
|
|
364
|
-
async def test_hermes_terminal_routes_to_sandbox(client: AsyncOpenReward, server: str):
|
|
365
|
-
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
366
|
-
tasks = await env.list_tasks(split="train")
|
|
367
|
-
async with env.session(tasks[0], toolset="hermes") as session:
|
|
368
|
-
result = await session.call_tool("terminal", {"command": "echo hi"})
|
|
369
|
-
assert "ran: echo hi" in result.blocks[0].text
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
@pytest.mark.asyncio
|
|
373
|
-
async def test_hermes_write_then_read_roundtrip(client: AsyncOpenReward, server: str):
|
|
374
|
-
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
375
|
-
tasks = await env.list_tasks(split="train")
|
|
376
|
-
async with env.session(tasks[0], toolset="hermes") as session:
|
|
377
|
-
await session.call_tool("write_file", {"path": "/tmp/hm.txt", "content": "hello hermes"})
|
|
378
|
-
result = await session.call_tool("read_file", {"path": "/tmp/hm.txt"})
|
|
379
|
-
# Hermes read_file uses LINE_NUM|CONTENT format
|
|
380
|
-
assert "hello hermes" in result.blocks[0].text
|
|
381
|
-
assert "1|" in result.blocks[0].text
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
@pytest.mark.asyncio
|
|
385
|
-
async def test_hermes_patch_replace_mode(client: AsyncOpenReward, server: str):
|
|
386
|
-
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
387
|
-
tasks = await env.list_tasks(split="train")
|
|
388
|
-
async with env.session(tasks[0], toolset="hermes") as session:
|
|
389
|
-
await session.call_tool("write_file", {"path": "/tmp/hm_patch.txt", "content": "foo bar baz"})
|
|
390
|
-
result = await session.call_tool("patch", {
|
|
391
|
-
"mode": "replace",
|
|
392
|
-
"path": "/tmp/hm_patch.txt",
|
|
393
|
-
"old_string": "bar",
|
|
394
|
-
"new_string": "qux",
|
|
395
|
-
})
|
|
396
|
-
assert "Successfully patched" in result.blocks[0].text
|
|
397
|
-
read_result = await session.call_tool("read_file", {"path": "/tmp/hm_patch.txt"})
|
|
398
|
-
assert "qux" in read_result.blocks[0].text
|
|
399
|
-
assert "bar" not in read_result.blocks[0].text
|
|
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
|
{openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/__init__.py
RENAMED
|
File without changes
|
{openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/ant.py
RENAMED
|
File without changes
|
{openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/base.py
RENAMED
|
File without changes
|
{openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/gdm.py
RENAMED
|
File without changes
|
{openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/utils.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/basic/requirements.txt
RENAMED
|
File without changes
|
{openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/basic/requirements.txt.tmpl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/sandbox/requirements.txt
RENAMED
|
File without changes
|
{openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/sandbox/sandbox_env.py
RENAMED
|
File without changes
|
|
File without changes
|
|
@@ -18,8 +18,8 @@ from .word import WordToolset
|
|
|
18
18
|
BUILTIN_TOOLSETS: dict[str, type[Toolset]] = {
|
|
19
19
|
ClaudeCodeToolset.name(): ClaudeCodeToolset,
|
|
20
20
|
CodexToolset.name(): CodexToolset,
|
|
21
|
-
OpenClawToolset.name(): OpenClawToolset,
|
|
22
21
|
HermesToolset.name(): HermesToolset,
|
|
22
|
+
OpenClawToolset.name(): OpenClawToolset,
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|