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.
Files changed (76) hide show
  1. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/PKG-INFO +1 -1
  2. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/environments/client.py +2 -2
  3. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/hermes.py +93 -22
  4. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/openclaw.py +148 -14
  5. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward.egg-info/PKG-INFO +1 -1
  6. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/pyproject.toml +1 -1
  7. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_session_toolset.py +0 -114
  8. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/README.md +0 -0
  9. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/__init__.py +0 -0
  10. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/_version.py +0 -0
  11. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/__init__.py +0 -0
  12. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/_session/__init__.py +0 -0
  13. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/_session/http.py +0 -0
  14. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/_session/ping.py +0 -0
  15. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/_session/session.py +0 -0
  16. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/environments/__init__.py +0 -0
  17. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/environments/types.py +0 -0
  18. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/__init__.py +0 -0
  19. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/background.py +0 -0
  20. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/rollout.py +0 -0
  21. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/__init__.py +0 -0
  22. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/ant.py +0 -0
  23. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/base.py +0 -0
  24. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/gdm.py +0 -0
  25. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/models.py +0 -0
  26. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/oai_completions.py +0 -0
  27. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/oai_responses.py +0 -0
  28. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/rollouts/serializers/utils.py +0 -0
  29. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/sandboxes/__init__.py +0 -0
  30. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/sandboxes/client.py +0 -0
  31. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/sandboxes/secrets.py +0 -0
  32. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/api/sandboxes/types.py +0 -0
  33. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/cli.py +0 -0
  34. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/client.py +0 -0
  35. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/__init__.py +0 -0
  36. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/environment.py +0 -0
  37. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/reconnect.py +0 -0
  38. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/server.py +0 -0
  39. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/session.py +0 -0
  40. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/toolset.py +0 -0
  41. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/types.py +0 -0
  42. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/environments/utils.py +0 -0
  43. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/http_client.py +0 -0
  44. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/log_utils.py +0 -0
  45. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/models.py +0 -0
  46. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/__init__.py +0 -0
  47. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/basic/Dockerfile +0 -0
  48. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/basic/__init__.py +0 -0
  49. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/basic/requirements.txt +0 -0
  50. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/basic/requirements.txt.tmpl +0 -0
  51. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/basic/server.py.tmpl +0 -0
  52. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/sandbox/Dockerfile +0 -0
  53. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/sandbox/__init__.py +0 -0
  54. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/sandbox/requirements.txt +0 -0
  55. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/sandbox/sandbox_env.py +0 -0
  56. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/templates/sandbox/server.py +0 -0
  57. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/__init__.py +1 -1
  58. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/claude_code.py +0 -0
  59. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/codex.py +0 -0
  60. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/excel.py +0 -0
  61. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/pdf.py +0 -0
  62. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/powerpoint.py +0 -0
  63. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward/toolsets/word.py +0 -0
  64. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward.egg-info/SOURCES.txt +0 -0
  65. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward.egg-info/dependency_links.txt +0 -0
  66. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward.egg-info/entry_points.txt +0 -0
  67. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward.egg-info/requires.txt +0 -0
  68. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/openreward.egg-info/top_level.txt +0 -0
  69. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/setup.cfg +0 -0
  70. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_environment.py +0 -0
  71. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_errors.py +0 -0
  72. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_rollout_info.py +0 -0
  73. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_sanitise.py +0 -0
  74. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_session.py +0 -0
  75. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_tool_conversion.py +0 -0
  76. {openreward-0.1.96.dev0 → openreward-0.1.96.dev2}/tests/test_toolsets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openreward
3
- Version: 0.1.96.dev0
3
+ Version: 0.1.96.dev2
4
4
  Summary: Python SDK for the OpenReward platform.
5
5
  Author-email: GR Inc <hello@gr.inc>
6
6
  Requires-Python: >=3.11
@@ -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", "openclaw", "hermes"]
16
- _VALID_BUILTIN_TOOLSETS = {"claude-code", "codex", "openclaw", "hermes"}
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 four built-in tools that Hermes Agent exposes for coding tasks
4
- (``terminal``, ``read_file``, ``write_file``, ``patch``), each backed by
5
- ``self.sandbox`` from the bound environment.
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 style) ──
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 four-tool coding surface.
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
- # Format as LINE_NUM|CONTENT (Hermes native format)
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 four built-in tools that OpenClaw exposes for coding tasks
4
- (``exec``, ``read``, ``write``, ``edit``), each backed by ``self.sandbox``
5
- from the bound environment.
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, Field
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
- # ── Tool descriptions (matching OpenClaw TypeBox style) ──
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 four-tool coding surface.
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 # Convert 1-indexed to 0-indexed
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openreward
3
- Version: 0.1.96.dev0
3
+ Version: 0.1.96.dev2
4
4
  Summary: Python SDK for the OpenReward platform.
5
5
  Author-email: GR Inc <hello@gr.inc>
6
6
  Requires-Python: >=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openreward"
3
- version = "0.1.96.dev0"
3
+ version = "0.1.96.dev2"
4
4
  description = "Python SDK for the OpenReward platform."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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
@@ -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__ = [