@pushpalsdev/cli 1.0.18 → 1.0.19
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.
- package/dist/pushpals-cli.js +277 -12
- package/package.json +1 -1
- package/runtime/sandbox/apps/workerpals/.python-version +1 -0
- package/runtime/sandbox/apps/workerpals/Dockerfile.sandbox +71 -0
- package/runtime/sandbox/apps/workerpals/package.json +25 -0
- package/runtime/sandbox/apps/workerpals/pyproject.toml +8 -0
- package/runtime/sandbox/apps/workerpals/src/backends/backend_config.ts +111 -0
- package/runtime/sandbox/apps/workerpals/src/backends/miniswe/miniswe_executor.py +2029 -0
- package/runtime/sandbox/apps/workerpals/src/backends/miniswe_backend.ts +48 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +1259 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +110 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex_backend.ts +67 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openhands/openhands_executor.py +563 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openhands_backend.ts +161 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openhands_task_execute.ts +536 -0
- package/runtime/sandbox/apps/workerpals/src/backends/shared/executor_base.py +746 -0
- package/runtime/sandbox/apps/workerpals/src/backends/shared/test_settings_resolver.py +60 -0
- package/runtime/sandbox/apps/workerpals/src/backends/task_execute_registry.ts +21 -0
- package/runtime/sandbox/apps/workerpals/src/backends/types.ts +52 -0
- package/runtime/sandbox/apps/workerpals/src/common/execution_utils.ts +149 -0
- package/runtime/sandbox/apps/workerpals/src/common/executor_backend.ts +15 -0
- package/runtime/sandbox/apps/workerpals/src/common/generic_python_executor.ts +210 -0
- package/runtime/sandbox/apps/workerpals/src/common/logger.ts +65 -0
- package/runtime/sandbox/apps/workerpals/src/common/types.ts +9 -0
- package/runtime/sandbox/apps/workerpals/src/common/worktree_cleanup.ts +66 -0
- package/runtime/sandbox/apps/workerpals/src/context_manager.ts +45 -0
- package/runtime/sandbox/apps/workerpals/src/docker_executor.ts +1842 -0
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +3063 -0
- package/runtime/sandbox/apps/workerpals/src/job_runner.ts +194 -0
- package/runtime/sandbox/apps/workerpals/src/shell_manager.ts +210 -0
- package/runtime/sandbox/apps/workerpals/src/timeout_policy.ts +24 -0
- package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +1436 -0
- package/runtime/sandbox/apps/workerpals/tsconfig.json +15 -0
- package/runtime/sandbox/apps/workerpals/uv.lock +2014 -0
- package/runtime/sandbox/bun.lock +2591 -0
- package/runtime/sandbox/configs/backend.toml +79 -0
- package/runtime/sandbox/configs/default.toml +260 -0
- package/runtime/sandbox/configs/dev.toml +2 -0
- package/runtime/sandbox/configs/local.example.toml +129 -0
- package/runtime/sandbox/package.json +65 -0
- package/runtime/sandbox/packages/protocol/README.md +168 -0
- package/runtime/sandbox/packages/protocol/package.json +37 -0
- package/runtime/sandbox/packages/protocol/scripts/copy-schemas.js +17 -0
- package/runtime/sandbox/packages/protocol/src/a2a/README.md +52 -0
- package/runtime/sandbox/packages/protocol/src/a2a/mapping.ts +55 -0
- package/runtime/sandbox/packages/protocol/src/index.browser.ts +25 -0
- package/runtime/sandbox/packages/protocol/src/index.ts +25 -0
- package/runtime/sandbox/packages/protocol/src/schemas/approvals.schema.json +6 -0
- package/runtime/sandbox/packages/protocol/src/schemas/envelope.schema.json +96 -0
- package/runtime/sandbox/packages/protocol/src/schemas/events.schema.json +679 -0
- package/runtime/sandbox/packages/protocol/src/schemas/http.schema.json +50 -0
- package/runtime/sandbox/packages/protocol/src/types.ts +267 -0
- package/runtime/sandbox/packages/protocol/src/validate.browser.ts +154 -0
- package/runtime/sandbox/packages/protocol/src/validate.ts +233 -0
- package/runtime/sandbox/packages/protocol/src/version.ts +1 -0
- package/runtime/sandbox/packages/protocol/tsconfig.json +20 -0
- package/runtime/sandbox/packages/shared/package.json +19 -0
- package/runtime/sandbox/packages/shared/src/autonomy_policy.ts +400 -0
- package/runtime/sandbox/packages/shared/src/client_preflight.ts +297 -0
- package/runtime/sandbox/packages/shared/src/communication.ts +313 -0
- package/runtime/sandbox/packages/shared/src/config.ts +2201 -0
- package/runtime/sandbox/packages/shared/src/config_template_parity.ts +70 -0
- package/runtime/sandbox/packages/shared/src/git_backend.ts +205 -0
- package/runtime/sandbox/packages/shared/src/index.ts +100 -0
- package/runtime/sandbox/packages/shared/src/local_network.ts +101 -0
- package/runtime/sandbox/packages/shared/src/localbuddy_runtime.ts +329 -0
- package/runtime/sandbox/packages/shared/src/prompts.ts +64 -0
- package/runtime/sandbox/packages/shared/src/repo.ts +134 -0
- package/runtime/sandbox/packages/shared/src/session_event_visibility.ts +25 -0
- package/runtime/sandbox/packages/shared/src/vision.ts +247 -0
- package/runtime/sandbox/packages/shared/tsconfig.json +16 -0
- package/runtime/sandbox/prompts/workerpals/codex_quality_critic_instruction_prompt.md +14 -0
- package/runtime/sandbox/prompts/workerpals/commit_message_prompt.md +36 -0
- package/runtime/sandbox/prompts/workerpals/commit_message_user_prompt.md +7 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_broker_system_prompt.md +33 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_broker_task_prompt.md +5 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_completion_requirement.md +1 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_context_compaction_retry_prompt.md +1 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_explicit_targets_block.md +2 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_base.md +4 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_blocker_line.md +1 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_strict_tool_use_guidance.md +6 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_supplemental_guidance_section.md +2 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_timeout_note.md +1 -0
- package/runtime/sandbox/prompts/workerpals/miniswe_toolcall_retry_guidance.md +1 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_default_system_prompt.md +4 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_instruction_wrapper.md +5 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_runtime_policy_appendix.md +5 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_supplemental_guidance_section.md +2 -0
- package/runtime/sandbox/prompts/workerpals/openai_codex_task_execute_system_prompt.md +12 -0
- package/runtime/sandbox/prompts/workerpals/openhands_minimal_security_policy.j2 +8 -0
- package/runtime/sandbox/prompts/workerpals/openhands_minimal_system_prompt.j2 +20 -0
- package/runtime/sandbox/prompts/workerpals/openhands_strict_tool_use_message.md +1 -0
- package/runtime/sandbox/prompts/workerpals/openhands_supplemental_guidance_message.md +2 -0
- package/runtime/sandbox/prompts/workerpals/openhands_task_execute_fallback_system_prompt.md +1 -0
- package/runtime/sandbox/prompts/workerpals/openhands_task_execute_system_prompt.md +21 -0
- package/runtime/sandbox/prompts/workerpals/openhands_task_user_prompt.md +6 -0
- package/runtime/sandbox/prompts/workerpals/openhands_timeout_note.md +1 -0
- package/runtime/sandbox/prompts/workerpals/pr_description.md +42 -0
- package/runtime/sandbox/prompts/workerpals/task_quality_critic_system_prompt.md +9 -0
- package/runtime/sandbox/prompts/workerpals/task_quality_critic_user_prompt.md +17 -0
- package/runtime/sandbox/prompts/workerpals/workerpals_system_prompt.md +115 -0
- package/runtime/sandbox/protocol/schemas/approvals.schema.json +6 -0
- package/runtime/sandbox/protocol/schemas/envelope.schema.json +96 -0
- package/runtime/sandbox/protocol/schemas/events.schema.json +679 -0
- package/runtime/sandbox/protocol/schemas/http.schema.json +50 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PushPals OpenHands executor wrapper.
|
|
3
|
+
|
|
4
|
+
Minimal wrapper contract:
|
|
5
|
+
- decode payload via executor_base
|
|
6
|
+
- run OpenHands SDK task.execute flow
|
|
7
|
+
- emit one structured result line
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
_SHARED_DIR = Path(__file__).resolve().parents[1] / "shared"
|
|
20
|
+
if str(_SHARED_DIR) not in sys.path:
|
|
21
|
+
sys.path.insert(0, str(_SHARED_DIR))
|
|
22
|
+
|
|
23
|
+
from executor_base import (
|
|
24
|
+
Logger,
|
|
25
|
+
emit,
|
|
26
|
+
is_no_tool_calls_error,
|
|
27
|
+
is_truthy_env,
|
|
28
|
+
log_git_status,
|
|
29
|
+
looks_local_base_url,
|
|
30
|
+
parse_task_execute_payload,
|
|
31
|
+
repo_root_for_runtime_config,
|
|
32
|
+
resolve_llm_config,
|
|
33
|
+
setting_int,
|
|
34
|
+
setting_str,
|
|
35
|
+
summarize_git_changes,
|
|
36
|
+
to_int,
|
|
37
|
+
to_single_line,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
LOG_PREFIX = "[OpenHandsExecutor]"
|
|
41
|
+
log = Logger(LOG_PREFIX)
|
|
42
|
+
DEFAULT_OPENHANDS_MODEL = "local-model"
|
|
43
|
+
PROMPT_TOKEN_REGEX = re.compile(r"\{\{\s*([a-zA-Z0-9_]+)\s*\}\}")
|
|
44
|
+
GLOB_META_REGEX = re.compile(r"[*?\[\]{}()!]")
|
|
45
|
+
_PROMPT_TEMPLATE_CACHE: Dict[str, str] = {}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _safe_session_component(value: Any, fallback: str = "unknown") -> str:
|
|
49
|
+
text = str(value or "").strip().lower()
|
|
50
|
+
if not text:
|
|
51
|
+
text = fallback
|
|
52
|
+
text = re.sub(r"[^a-z0-9._:-]+", "-", text)
|
|
53
|
+
text = re.sub(r"-{2,}", "-", text).strip("-")
|
|
54
|
+
if not text:
|
|
55
|
+
text = fallback
|
|
56
|
+
return text[:64]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _stable_llm_session_user(payload: Optional[Dict[str, Any]]) -> str:
|
|
60
|
+
override = setting_str("WORKERPALS_LLM_SESSION_ID", "workerpals.llm.session_id", "")
|
|
61
|
+
if override:
|
|
62
|
+
return _safe_session_component(override, "pushpals-worker")
|
|
63
|
+
|
|
64
|
+
session_id = _safe_session_component(setting_str("PUSHPALS_SESSION_ID", "session_id", ""), "session")
|
|
65
|
+
worker_id = _safe_session_component((payload or {}).get("workerId"), "worker")
|
|
66
|
+
task_id = _safe_session_component((payload or {}).get("taskId"), "task")
|
|
67
|
+
return f"pushpals-{session_id}-{worker_id}-{task_id}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _session_hint_headers(session_user: str) -> Dict[str, str]:
|
|
71
|
+
if not session_user:
|
|
72
|
+
return {}
|
|
73
|
+
return {
|
|
74
|
+
"X-PushPals-Session-Id": session_user,
|
|
75
|
+
"X-Session-Id": session_user,
|
|
76
|
+
"X-Conversation-Id": session_user,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _repo_root_for_prompt_loading() -> Path:
|
|
81
|
+
return repo_root_for_runtime_config()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _resolve_prompt_file(relative_path: str) -> Path:
|
|
85
|
+
return _repo_root_for_prompt_loading() / "prompts" / relative_path
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _load_prompt_template(relative_path: str, replacements: Optional[Dict[str, str]] = None) -> str:
|
|
89
|
+
prompt_path = _resolve_prompt_file(relative_path)
|
|
90
|
+
prompt_key = str(prompt_path)
|
|
91
|
+
|
|
92
|
+
template = _PROMPT_TEMPLATE_CACHE.get(prompt_key)
|
|
93
|
+
if template is None:
|
|
94
|
+
if not prompt_path.exists():
|
|
95
|
+
raise FileNotFoundError(f"Prompt template not found: {prompt_path}")
|
|
96
|
+
template = prompt_path.read_text(encoding="utf-8")
|
|
97
|
+
_PROMPT_TEMPLATE_CACHE[prompt_key] = template
|
|
98
|
+
|
|
99
|
+
if not replacements:
|
|
100
|
+
return template
|
|
101
|
+
|
|
102
|
+
def _replace(match: re.Match[str]) -> str:
|
|
103
|
+
key = match.group(1)
|
|
104
|
+
if key not in replacements:
|
|
105
|
+
raise KeyError(f"Missing prompt replacement '{{{{{key}}}}}' for {prompt_path}")
|
|
106
|
+
return replacements[key]
|
|
107
|
+
|
|
108
|
+
return PROMPT_TOKEN_REGEX.sub(_replace, template)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _resolve_agent_prompt_overrides(base_url: str) -> Dict[str, Any]:
|
|
112
|
+
profile = setting_str(
|
|
113
|
+
"WORKERPALS_OPENHANDS_PROMPT_PROFILE",
|
|
114
|
+
"workerpals.openhands.prompt_profile",
|
|
115
|
+
"minimal" if looks_local_base_url(base_url) else "default",
|
|
116
|
+
).lower()
|
|
117
|
+
if profile not in {"minimal", "compact", "small"}:
|
|
118
|
+
return {}
|
|
119
|
+
|
|
120
|
+
overrides: Dict[str, Any] = {}
|
|
121
|
+
system_prompt = _resolve_prompt_file("workerpals/openhands_minimal_system_prompt.j2")
|
|
122
|
+
security_prompt = _resolve_prompt_file("workerpals/openhands_minimal_security_policy.j2")
|
|
123
|
+
if system_prompt.exists():
|
|
124
|
+
overrides["system_prompt_filename"] = str(system_prompt)
|
|
125
|
+
if security_prompt.exists():
|
|
126
|
+
overrides["security_policy_filename"] = str(security_prompt)
|
|
127
|
+
return overrides
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _json_object_from_env(name: str) -> Tuple[Optional[Dict[str, Any]], str]:
|
|
131
|
+
raw = (os.environ.get(name) or "").strip()
|
|
132
|
+
if not raw:
|
|
133
|
+
return None, ""
|
|
134
|
+
try:
|
|
135
|
+
parsed = json.loads(raw)
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
return None, f"{name}: invalid JSON ({exc})"
|
|
138
|
+
if not isinstance(parsed, dict):
|
|
139
|
+
return None, f"{name}: expected a JSON object"
|
|
140
|
+
return parsed, ""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _resolve_mcp_config() -> Tuple[Optional[Dict[str, Any]], List[str]]:
|
|
144
|
+
notes: List[str] = []
|
|
145
|
+
config: Optional[Dict[str, Any]] = None
|
|
146
|
+
|
|
147
|
+
raw = setting_str("WORKERPALS_OPENHANDS_MCP_CONFIG_JSON", "workerpals.openhands.mcp_config_json", "")
|
|
148
|
+
if raw:
|
|
149
|
+
try:
|
|
150
|
+
parsed = json.loads(raw)
|
|
151
|
+
if isinstance(parsed, dict):
|
|
152
|
+
config = parsed
|
|
153
|
+
else:
|
|
154
|
+
notes.append(f"{LOG_PREFIX} Ignoring MCP config: expected JSON object.")
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
notes.append(f"{LOG_PREFIX} Ignoring MCP config JSON: {exc}")
|
|
157
|
+
|
|
158
|
+
if not is_truthy_env("WORKERPALS_OPENHANDS_ENABLE_WEB_MCP", False, "workerpals.openhands.enable_web_mcp"):
|
|
159
|
+
return config, notes
|
|
160
|
+
|
|
161
|
+
web_url = setting_str("WORKERPALS_OPENHANDS_WEB_MCP_URL", "workerpals.openhands.web_mcp_url", "")
|
|
162
|
+
if not web_url:
|
|
163
|
+
notes.append(f"{LOG_PREFIX} Web MCP enabled but URL is empty; skipping.")
|
|
164
|
+
return config, notes
|
|
165
|
+
|
|
166
|
+
server_name = setting_str(
|
|
167
|
+
"WORKERPALS_OPENHANDS_WEB_MCP_NAME",
|
|
168
|
+
"workerpals.openhands.web_mcp_name",
|
|
169
|
+
"web-search",
|
|
170
|
+
)
|
|
171
|
+
transport = setting_str(
|
|
172
|
+
"WORKERPALS_OPENHANDS_WEB_MCP_TRANSPORT",
|
|
173
|
+
"workerpals.openhands.web_mcp_transport",
|
|
174
|
+
"streamable-http",
|
|
175
|
+
).lower().replace("streamable_http", "streamable-http")
|
|
176
|
+
if transport not in {"http", "streamable-http", "sse"}:
|
|
177
|
+
transport = "streamable-http"
|
|
178
|
+
|
|
179
|
+
headers, headers_error = _json_object_from_env("WORKERPALS_OPENHANDS_WEB_MCP_HEADERS_JSON")
|
|
180
|
+
if headers_error:
|
|
181
|
+
notes.append(f"{LOG_PREFIX} Ignoring MCP headers JSON: {headers_error}")
|
|
182
|
+
headers = None
|
|
183
|
+
|
|
184
|
+
auth_token = (os.environ.get("WORKERPALS_OPENHANDS_WEB_MCP_AUTH_TOKEN") or "").strip()
|
|
185
|
+
timeout_sec = setting_int(
|
|
186
|
+
"WORKERPALS_OPENHANDS_WEB_MCP_TIMEOUT_SEC",
|
|
187
|
+
"workerpals.openhands.web_mcp_timeout_sec",
|
|
188
|
+
0,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
server_config: Dict[str, Any] = {"url": web_url, "transport": transport}
|
|
192
|
+
if headers:
|
|
193
|
+
server_config["headers"] = {
|
|
194
|
+
str(k): str(v)
|
|
195
|
+
for k, v in headers.items()
|
|
196
|
+
if isinstance(k, str) and isinstance(v, (str, int, float, bool))
|
|
197
|
+
}
|
|
198
|
+
if auth_token:
|
|
199
|
+
server_config["auth"] = auth_token
|
|
200
|
+
if timeout_sec > 0:
|
|
201
|
+
server_config["timeout"] = timeout_sec
|
|
202
|
+
|
|
203
|
+
if config is None:
|
|
204
|
+
config = {"mcpServers": {}}
|
|
205
|
+
if not isinstance(config.get("mcpServers"), dict):
|
|
206
|
+
config["mcpServers"] = {}
|
|
207
|
+
config["mcpServers"][server_name] = server_config
|
|
208
|
+
notes.append(f"{LOG_PREFIX} Web MCP enabled: {server_name} -> {web_url} ({transport})")
|
|
209
|
+
return config, notes
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _browser_tool_enabled() -> bool:
|
|
213
|
+
return is_truthy_env(
|
|
214
|
+
"WORKERPALS_OPENHANDS_ENABLE_BROWSER_TOOL",
|
|
215
|
+
False,
|
|
216
|
+
"workerpals.openhands.enable_browser_tool",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _build_user_message(instruction: str, timeout_ms: int) -> str:
|
|
221
|
+
timeout_minutes = max(1, round(timeout_ms / 60000))
|
|
222
|
+
timeout_note = _load_prompt_template(
|
|
223
|
+
"workerpals/openhands_timeout_note.md",
|
|
224
|
+
{"timeout_minutes": str(timeout_minutes)},
|
|
225
|
+
).strip()
|
|
226
|
+
|
|
227
|
+
mode = setting_str(
|
|
228
|
+
"WORKERPALS_OPENHANDS_TASK_PROMPT_MODE",
|
|
229
|
+
"workerpals.openhands.task_prompt_mode",
|
|
230
|
+
"none",
|
|
231
|
+
).lower()
|
|
232
|
+
if mode in {"none", "off", "instruction_only", "instruction-only", "minimal"}:
|
|
233
|
+
return f"{instruction}\n\n{timeout_note}"
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
system_prompt = _load_prompt_template("workerpals/openhands_task_execute_system_prompt.md").strip()
|
|
237
|
+
except Exception:
|
|
238
|
+
system_prompt = _load_prompt_template(
|
|
239
|
+
"workerpals/openhands_task_execute_fallback_system_prompt.md"
|
|
240
|
+
).strip()
|
|
241
|
+
return _load_prompt_template(
|
|
242
|
+
"workerpals/openhands_task_user_prompt.md",
|
|
243
|
+
{
|
|
244
|
+
"system_prompt": system_prompt,
|
|
245
|
+
"instruction": instruction,
|
|
246
|
+
"timeout_note": timeout_note,
|
|
247
|
+
},
|
|
248
|
+
).strip()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _build_strict_tool_use_message() -> str:
|
|
252
|
+
return _load_prompt_template("workerpals/openhands_strict_tool_use_message.md").strip()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _normalize_repo_relative_path(value: Any) -> Optional[str]:
|
|
256
|
+
if not isinstance(value, str):
|
|
257
|
+
return None
|
|
258
|
+
path = value.strip().replace("\\", "/")
|
|
259
|
+
if not path:
|
|
260
|
+
return None
|
|
261
|
+
if path == "/repo" or path == "/workspace":
|
|
262
|
+
return "."
|
|
263
|
+
if path.startswith("/repo/"):
|
|
264
|
+
path = path[len("/repo/") :]
|
|
265
|
+
elif path.startswith("/workspace/"):
|
|
266
|
+
path = path[len("/workspace/") :]
|
|
267
|
+
elif path.startswith("/"):
|
|
268
|
+
return None
|
|
269
|
+
if re.match(r"^[A-Za-z]:[\\/]", path):
|
|
270
|
+
return None
|
|
271
|
+
while path.startswith("./"):
|
|
272
|
+
path = path[2:]
|
|
273
|
+
path = re.sub(r"/+", "/", path).strip("/")
|
|
274
|
+
if not path:
|
|
275
|
+
return None
|
|
276
|
+
segments = path.split("/")
|
|
277
|
+
out: List[str] = []
|
|
278
|
+
for segment in segments:
|
|
279
|
+
segment = segment.strip()
|
|
280
|
+
if not segment or segment == ".":
|
|
281
|
+
continue
|
|
282
|
+
if segment == "..":
|
|
283
|
+
return None
|
|
284
|
+
out.append(segment)
|
|
285
|
+
if not out:
|
|
286
|
+
return None
|
|
287
|
+
return "/".join(out)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _add_literal_path(values: List[str], seen: set[str], raw: Any) -> None:
|
|
291
|
+
normalized = _normalize_repo_relative_path(raw)
|
|
292
|
+
if not normalized or normalized == ".":
|
|
293
|
+
return
|
|
294
|
+
if GLOB_META_REGEX.search(normalized):
|
|
295
|
+
return
|
|
296
|
+
key = normalized.lower()
|
|
297
|
+
if key in seen:
|
|
298
|
+
return
|
|
299
|
+
seen.add(key)
|
|
300
|
+
values.append(normalized)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _extract_target_paths(payload: Optional[Dict[str, Any]]) -> List[str]:
|
|
304
|
+
out: List[str] = []
|
|
305
|
+
seen: set[str] = set()
|
|
306
|
+
if not isinstance(payload, dict):
|
|
307
|
+
return out
|
|
308
|
+
params = payload.get("params")
|
|
309
|
+
if not isinstance(params, dict):
|
|
310
|
+
return out
|
|
311
|
+
|
|
312
|
+
_add_literal_path(out, seen, params.get("targetPath"))
|
|
313
|
+
_add_literal_path(out, seen, params.get("path"))
|
|
314
|
+
paths = params.get("paths")
|
|
315
|
+
if isinstance(paths, list):
|
|
316
|
+
for entry in paths:
|
|
317
|
+
_add_literal_path(out, seen, entry)
|
|
318
|
+
if len(out) >= 8:
|
|
319
|
+
return out
|
|
320
|
+
|
|
321
|
+
planning = params.get("planning")
|
|
322
|
+
if not isinstance(planning, dict):
|
|
323
|
+
return out
|
|
324
|
+
target_paths = planning.get("targetPaths")
|
|
325
|
+
if isinstance(target_paths, list):
|
|
326
|
+
for entry in target_paths:
|
|
327
|
+
_add_literal_path(out, seen, entry)
|
|
328
|
+
if len(out) >= 8:
|
|
329
|
+
return out
|
|
330
|
+
scope = planning.get("scope")
|
|
331
|
+
if isinstance(scope, dict):
|
|
332
|
+
write_globs = scope.get("writeGlobs")
|
|
333
|
+
if isinstance(write_globs, list):
|
|
334
|
+
for entry in write_globs:
|
|
335
|
+
_add_literal_path(out, seen, entry)
|
|
336
|
+
if len(out) >= 8:
|
|
337
|
+
return out
|
|
338
|
+
return out
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _build_path_handling_message(target_paths: List[str], repo: str) -> str:
|
|
342
|
+
if not target_paths:
|
|
343
|
+
return ""
|
|
344
|
+
rel_paths = target_paths[:8]
|
|
345
|
+
listed_rel = "\n".join(f"- {path}" for path in rel_paths)
|
|
346
|
+
repo_root = str(Path(repo).resolve()).replace("\\", "/").rstrip("/")
|
|
347
|
+
abs_paths = [f"{repo_root}/{path}" for path in rel_paths]
|
|
348
|
+
listed_abs = "\n".join(f"- {path}" for path in abs_paths)
|
|
349
|
+
return (
|
|
350
|
+
"Path handling requirements:\n"
|
|
351
|
+
"- The current working directory is the repository root.\n"
|
|
352
|
+
"- Prefer the repo-relative paths for shell commands.\n"
|
|
353
|
+
"- If FileEditor rejects a repo-relative path, retry with the matching absolute path.\n"
|
|
354
|
+
"- Do not run broad filesystem scans when concrete target paths are listed.\n"
|
|
355
|
+
"Concrete target paths (repo-relative):\n"
|
|
356
|
+
f"{listed_rel}\n"
|
|
357
|
+
"Concrete target paths (absolute):\n"
|
|
358
|
+
f"{listed_abs}"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _run_openhands_task(
|
|
363
|
+
repo: str,
|
|
364
|
+
instruction: str,
|
|
365
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
366
|
+
supplemental_guidance: Optional[List[str]] = None,
|
|
367
|
+
) -> Dict[str, Any]:
|
|
368
|
+
try:
|
|
369
|
+
from openhands.sdk import Agent, Conversation, LLM, Tool
|
|
370
|
+
from openhands.tools.file_editor import FileEditorTool
|
|
371
|
+
from openhands.tools.terminal import TerminalTool
|
|
372
|
+
except Exception as exc:
|
|
373
|
+
return {
|
|
374
|
+
"ok": False,
|
|
375
|
+
"summary": "OpenHands SDK is not installed. Install with: pip install openhands-ai",
|
|
376
|
+
"stderr": str(exc),
|
|
377
|
+
"exitCode": 3,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
model, api_key, base_url = resolve_llm_config(
|
|
381
|
+
default_model=DEFAULT_OPENHANDS_MODEL,
|
|
382
|
+
logger=log,
|
|
383
|
+
)
|
|
384
|
+
if not model:
|
|
385
|
+
return {
|
|
386
|
+
"ok": False,
|
|
387
|
+
"summary": "task.execute requires an LLM model. Set WORKERPALS_LLM_MODEL.",
|
|
388
|
+
"stderr": "",
|
|
389
|
+
"exitCode": 2,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if not api_key:
|
|
393
|
+
if looks_local_base_url(base_url):
|
|
394
|
+
api_key = "local"
|
|
395
|
+
else:
|
|
396
|
+
return {
|
|
397
|
+
"ok": False,
|
|
398
|
+
"summary": "task.execute requires an API key. Set WORKERPALS_LLM_API_KEY.",
|
|
399
|
+
"stderr": "",
|
|
400
|
+
"exitCode": 2,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
llm_kwargs: Dict[str, Any] = {
|
|
404
|
+
"model": model,
|
|
405
|
+
"api_key": api_key,
|
|
406
|
+
}
|
|
407
|
+
if base_url:
|
|
408
|
+
llm_kwargs["base_url"] = base_url
|
|
409
|
+
|
|
410
|
+
session_user = _stable_llm_session_user(payload)
|
|
411
|
+
if session_user:
|
|
412
|
+
llm_kwargs["litellm_extra_body"] = {
|
|
413
|
+
"user": session_user,
|
|
414
|
+
"session_id": session_user,
|
|
415
|
+
"conversation_id": session_user,
|
|
416
|
+
}
|
|
417
|
+
llm_kwargs["extra_headers"] = _session_hint_headers(session_user)
|
|
418
|
+
|
|
419
|
+
max_steps = max(
|
|
420
|
+
1,
|
|
421
|
+
setting_int(
|
|
422
|
+
"WORKERPALS_OPENHANDS_AGENT_MAX_STEPS",
|
|
423
|
+
"workerpals.openhands.agent_max_steps",
|
|
424
|
+
30,
|
|
425
|
+
),
|
|
426
|
+
)
|
|
427
|
+
timeout_ms = to_int((payload or {}).get("timeoutMs"), 0)
|
|
428
|
+
if timeout_ms <= 0:
|
|
429
|
+
timeout_ms = max(
|
|
430
|
+
10_000,
|
|
431
|
+
setting_int("WORKERPALS_OPENHANDS_TIMEOUT_MS", "workerpals.openhands_timeout_ms", 300_000),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
mcp_config, mcp_notes = _resolve_mcp_config()
|
|
435
|
+
for note in mcp_notes:
|
|
436
|
+
log.info(note.removeprefix(f"{LOG_PREFIX} ").strip())
|
|
437
|
+
|
|
438
|
+
tools = [Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name)]
|
|
439
|
+
if _browser_tool_enabled():
|
|
440
|
+
try:
|
|
441
|
+
from openhands.tools.browser_use import BrowserToolSet
|
|
442
|
+
|
|
443
|
+
tools.append(Tool(name=BrowserToolSet.name))
|
|
444
|
+
log.info("BrowserToolSet enabled.")
|
|
445
|
+
except Exception as exc:
|
|
446
|
+
log.info(f"Browser tooling unavailable: {to_single_line(exc, 300)}")
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
llm = LLM(**llm_kwargs)
|
|
450
|
+
|
|
451
|
+
agent_kwargs: Dict[str, Any] = {"llm": llm, "tools": tools}
|
|
452
|
+
agent_kwargs.update(_resolve_agent_prompt_overrides(base_url))
|
|
453
|
+
if mcp_config:
|
|
454
|
+
agent_kwargs["mcp_config"] = mcp_config
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
agent = Agent(**agent_kwargs)
|
|
458
|
+
except TypeError:
|
|
459
|
+
# Older SDKs may not support prompt overrides/mcp_config.
|
|
460
|
+
agent = Agent(llm=llm, tools=tools)
|
|
461
|
+
|
|
462
|
+
conversation = Conversation(agent=agent, workspace=repo)
|
|
463
|
+
log.debug(f"Instruction: {to_single_line(instruction, 300)}")
|
|
464
|
+
conversation.send_message(_build_user_message(instruction, timeout_ms))
|
|
465
|
+
path_handling = _build_path_handling_message(_extract_target_paths(payload), repo)
|
|
466
|
+
if path_handling:
|
|
467
|
+
conversation.send_message(path_handling)
|
|
468
|
+
if supplemental_guidance:
|
|
469
|
+
for guidance in supplemental_guidance:
|
|
470
|
+
trimmed = str(guidance or "").strip()
|
|
471
|
+
if trimmed:
|
|
472
|
+
conversation.send_message(
|
|
473
|
+
_load_prompt_template(
|
|
474
|
+
"workerpals/openhands_supplemental_guidance_message.md",
|
|
475
|
+
{"guidance": trimmed},
|
|
476
|
+
).strip()
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
conversation.run(max_steps=max_steps)
|
|
481
|
+
except TypeError:
|
|
482
|
+
conversation.run()
|
|
483
|
+
except Exception as run_exc:
|
|
484
|
+
if is_no_tool_calls_error(run_exc):
|
|
485
|
+
# One strict nudge before surfacing failure.
|
|
486
|
+
conversation.send_message(_build_strict_tool_use_message())
|
|
487
|
+
try:
|
|
488
|
+
conversation.run(max_steps=max_steps)
|
|
489
|
+
except TypeError:
|
|
490
|
+
conversation.run()
|
|
491
|
+
except Exception:
|
|
492
|
+
raise run_exc
|
|
493
|
+
else:
|
|
494
|
+
raise
|
|
495
|
+
|
|
496
|
+
log.debug("Agent execution completed.")
|
|
497
|
+
# Log conversation events if the SDK exposes them
|
|
498
|
+
try:
|
|
499
|
+
events = getattr(conversation, "events", None) or getattr(conversation, "get_events", lambda: None)()
|
|
500
|
+
if events:
|
|
501
|
+
log.debug(f"Conversation events ({len(events)}):")
|
|
502
|
+
for i, event in enumerate(events[:30], 1):
|
|
503
|
+
if isinstance(event, dict):
|
|
504
|
+
action = event.get("action") or event.get("type") or "unknown"
|
|
505
|
+
args = event.get("args") or {}
|
|
506
|
+
path = args.get("path") or args.get("command") or ""
|
|
507
|
+
log.debug(f" Event {i}: {action} {to_single_line(str(path), 120)}")
|
|
508
|
+
else:
|
|
509
|
+
log.debug(f" Event {i}: {to_single_line(str(event), 150)}")
|
|
510
|
+
except Exception:
|
|
511
|
+
pass
|
|
512
|
+
log_git_status(repo, log)
|
|
513
|
+
|
|
514
|
+
except Exception as exc:
|
|
515
|
+
if is_no_tool_calls_error(exc):
|
|
516
|
+
return {
|
|
517
|
+
"ok": False,
|
|
518
|
+
"summary": "OpenHands could not execute: model did not emit tool calls/actions",
|
|
519
|
+
"stderr": (
|
|
520
|
+
"Agentic execution requires a tool-capable model/runtime. "
|
|
521
|
+
"The agent did not produce tool calls/actions.\n"
|
|
522
|
+
f"Error: {to_single_line(exc, 700)}"
|
|
523
|
+
),
|
|
524
|
+
"exitCode": 3,
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
"ok": False,
|
|
528
|
+
"summary": "OpenHands task execution failed",
|
|
529
|
+
"stderr": str(exc),
|
|
530
|
+
"exitCode": 1,
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
changed_paths = summarize_git_changes(repo)
|
|
534
|
+
if changed_paths:
|
|
535
|
+
listed = "\n".join(f"- {path}" for path in changed_paths[:40])
|
|
536
|
+
if len(changed_paths) > 40:
|
|
537
|
+
listed += "\n- ..."
|
|
538
|
+
return {
|
|
539
|
+
"ok": True,
|
|
540
|
+
"summary": f"Executed task and modified {len(changed_paths)} file(s)",
|
|
541
|
+
"stdout": f"Changed files:\n{listed}",
|
|
542
|
+
"stderr": "",
|
|
543
|
+
"exitCode": 0,
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
"ok": True,
|
|
548
|
+
"summary": "Executed task via OpenHands (no file changes detected)",
|
|
549
|
+
"stdout": "No modified files were detected after execution.",
|
|
550
|
+
"stderr": "",
|
|
551
|
+
"exitCode": 0,
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def main() -> int:
|
|
556
|
+
task = parse_task_execute_payload(sys.argv, logger=log)
|
|
557
|
+
result = _run_openhands_task(task.repo, task.instruction, task.payload, task.supplemental_guidance)
|
|
558
|
+
emit(result)
|
|
559
|
+
return 0 if bool(result.get("ok")) else to_int(result.get("exitCode"), 1)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
if __name__ == "__main__":
|
|
563
|
+
raise SystemExit(main())
|