@pushpalsdev/cli 1.0.18 → 1.0.20
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 +291 -44
- package/package.json +1 -1
- package/runtime/configs/backend.toml +1 -1
- package/runtime/configs/default.toml +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 +119 -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 +286 -0
- package/runtime/sandbox/packages/shared/src/communication.ts +313 -0
- package/runtime/sandbox/packages/shared/src/config.ts +2180 -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 +101 -0
- package/runtime/sandbox/packages/shared/src/local_network.ts +101 -0
- package/runtime/sandbox/packages/shared/src/localbuddy_runtime.ts +314 -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,161 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DockerBackendSpec,
|
|
3
|
+
DockerWarmRuntimeContext,
|
|
4
|
+
DockerWarmStartupContext,
|
|
5
|
+
} from "./types.js";
|
|
6
|
+
import { executeWithOpenHands } from "./openhands_task_execute.js";
|
|
7
|
+
|
|
8
|
+
function normalizeContainerPython(configuredPython: string, sharedVenvPython: string): string {
|
|
9
|
+
const configured = configuredPython.trim();
|
|
10
|
+
if (!configured) {
|
|
11
|
+
return sharedVenvPython;
|
|
12
|
+
}
|
|
13
|
+
const lowered = configured.toLowerCase();
|
|
14
|
+
if (
|
|
15
|
+
lowered === "python" ||
|
|
16
|
+
lowered === "python3" ||
|
|
17
|
+
configured.includes("\\") ||
|
|
18
|
+
/^[a-zA-Z]:/.test(configured) ||
|
|
19
|
+
configured.startsWith(".")
|
|
20
|
+
) {
|
|
21
|
+
return sharedVenvPython;
|
|
22
|
+
}
|
|
23
|
+
return configured;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function openHandsResolvePythonCommand(sharedVenvPython: string): string {
|
|
27
|
+
return (
|
|
28
|
+
`PY="\${WORKERPALS_OPENHANDS_PYTHON:-${sharedVenvPython}}"; ` +
|
|
29
|
+
'if [ ! -x "$PY" ]; then PY="$(command -v python3 || command -v python || true)"; fi; ' +
|
|
30
|
+
'[ -n "$PY" ] || { echo "python runtime not found" >&2; exit 1; }'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function openHandsHealthCommand(port: number): string {
|
|
35
|
+
return (
|
|
36
|
+
`curl -fsS http://127.0.0.1:${port}/health >/dev/null 2>&1 ` +
|
|
37
|
+
`|| curl -fsS http://127.0.0.1:${port}/ >/dev/null 2>&1`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function openHandsStartupCommand(context: DockerWarmStartupContext): string {
|
|
42
|
+
const { sharedVenvPython, warmAgentPort: port, startupAttempts, sleepSeconds } = context;
|
|
43
|
+
const healthCmd = openHandsHealthCommand(port);
|
|
44
|
+
const resolvePythonCmd = openHandsResolvePythonCommand(sharedVenvPython);
|
|
45
|
+
return (
|
|
46
|
+
`${resolvePythonCmd}; ` +
|
|
47
|
+
": >/tmp/openhands-agent.log; " +
|
|
48
|
+
`"$PY" -m openhands.agent_server --host 127.0.0.1 --port ${port} >/tmp/openhands-agent.log 2>&1 & ` +
|
|
49
|
+
`for i in $(seq 1 ${startupAttempts}); do ${healthCmd} && break; sleep ${sleepSeconds}; done; ` +
|
|
50
|
+
`${healthCmd} || { ` +
|
|
51
|
+
'echo "agent server health check failed"; ' +
|
|
52
|
+
'ps -ef | grep -i "openhands.agent_server" | grep -v grep || true; ' +
|
|
53
|
+
"ls -l /tmp/openhands-agent.log 2>/dev/null || true; " +
|
|
54
|
+
"tail -n 160 /tmp/openhands-agent.log 2>/dev/null; " +
|
|
55
|
+
"exit 1; }; " +
|
|
56
|
+
"tail -f /dev/null"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function openHandsRestartCommand(context: DockerWarmStartupContext): string {
|
|
61
|
+
const { sharedVenvPython, warmAgentPort: port, startupAttempts, sleepSeconds } = context;
|
|
62
|
+
const healthCmd = openHandsHealthCommand(port);
|
|
63
|
+
const resolvePythonCmd = openHandsResolvePythonCommand(sharedVenvPython);
|
|
64
|
+
return (
|
|
65
|
+
"OLD_PIDS=\"$(ps -eo pid,args | awk '/[o]penhands\\.agent_server/ {print $1}' | tr '\\n' ' ')\"; " +
|
|
66
|
+
'if [ -n "$OLD_PIDS" ]; then kill $OLD_PIDS >/dev/null 2>&1 || true; fi; ' +
|
|
67
|
+
"sleep 0.2; " +
|
|
68
|
+
`${resolvePythonCmd}; ` +
|
|
69
|
+
": >/tmp/openhands-agent.log; " +
|
|
70
|
+
`"$PY" -m openhands.agent_server --host 127.0.0.1 --port ${port} >/tmp/openhands-agent.log 2>&1 & ` +
|
|
71
|
+
`for i in $(seq 1 ${startupAttempts}); do ${healthCmd} && break; sleep ${sleepSeconds}; done; ` +
|
|
72
|
+
healthCmd
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function openHandsDiagnosticChecks(sharedVenvPython: string): Array<{
|
|
77
|
+
label: string;
|
|
78
|
+
command: string;
|
|
79
|
+
}> {
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
label: "processes",
|
|
83
|
+
command: 'ps -ef | grep -i "openhands.agent_server" | grep -v grep || true',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
label: "python",
|
|
87
|
+
command:
|
|
88
|
+
`${openHandsResolvePythonCommand(sharedVenvPython)}; ` +
|
|
89
|
+
'echo "configured=$PY"; ' +
|
|
90
|
+
'if [ -x "$PY" ]; then "$PY" -V 2>&1; else echo "configured python missing"; fi; ' +
|
|
91
|
+
"(command -v python3 && python3 -V) 2>/dev/null || true",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
label: "agent-log-meta",
|
|
95
|
+
command: "ls -l /tmp/openhands-agent.log 2>/dev/null || true",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
label: "agent-log-tail",
|
|
99
|
+
command: "tail -n 160 /tmp/openhands-agent.log 2>/dev/null || true",
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function ensureOpenHandsWarmRuntime(context: DockerWarmRuntimeContext): Promise<void> {
|
|
105
|
+
const healthCmd = openHandsHealthCommand(context.warmAgentPort);
|
|
106
|
+
const healthy = await context.runWarmShell(healthCmd);
|
|
107
|
+
if (healthy.ok) return;
|
|
108
|
+
|
|
109
|
+
console.warn(
|
|
110
|
+
`[DockerExecutor] Warm agent server is unhealthy in ${context.warmContainerName}; restarting it...`,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const restarted = await context.runWarmShell(openHandsRestartCommand(context));
|
|
114
|
+
if (restarted.ok) return;
|
|
115
|
+
|
|
116
|
+
let recreateError = "";
|
|
117
|
+
try {
|
|
118
|
+
console.warn(
|
|
119
|
+
`[DockerExecutor] Warm agent restart failed in ${context.warmContainerName}; recreating warm container once...`,
|
|
120
|
+
);
|
|
121
|
+
await context.restartWarmContainer();
|
|
122
|
+
const postRecreateHealth = await context.runWarmShell(
|
|
123
|
+
`for i in $(seq 1 ${context.startupAttempts}); do ${healthCmd} && exit 0; sleep ${context.sleepSeconds}; done; exit 1`,
|
|
124
|
+
);
|
|
125
|
+
if (postRecreateHealth.ok) return;
|
|
126
|
+
const postRecreateOutput = [postRecreateHealth.stderr, postRecreateHealth.stdout]
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.join("\n")
|
|
129
|
+
.trim();
|
|
130
|
+
recreateError = `post-recreate health check failed (exit ${postRecreateHealth.exitCode})${
|
|
131
|
+
postRecreateOutput ? `: ${postRecreateOutput}` : "."
|
|
132
|
+
}`;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
recreateError = `recreate warm container failed: ${
|
|
135
|
+
error instanceof Error ? error.message : String(error)
|
|
136
|
+
}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const restartOutput = [restarted.stderr, restarted.stdout].filter(Boolean).join("\n").trim();
|
|
140
|
+
const diagnostics = await context.collectWarmDiagnostics();
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Warm OpenHands agent server could not be started (exit ${restarted.exitCode})${
|
|
143
|
+
restartOutput ? `: ${restartOutput}` : "."
|
|
144
|
+
}${recreateError ? `\n${recreateError}` : ""}\n${diagnostics}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const OPENHANDS_BACKEND: DockerBackendSpec = {
|
|
149
|
+
name: "openhands",
|
|
150
|
+
configuredPython: (config) => config.openhands?.python ?? "python",
|
|
151
|
+
timeoutMs: (config) => config.openhands?.timeoutMs ?? 300_000,
|
|
152
|
+
normalizeContainerPython,
|
|
153
|
+
warmContainerStartupCommand: openHandsStartupCommand,
|
|
154
|
+
warmContainerEnv: (context) => ({
|
|
155
|
+
WORKERPALS_OPENHANDS_AGENT_SERVER_URL: `http://127.0.0.1:${context.warmAgentPort}`,
|
|
156
|
+
}),
|
|
157
|
+
ensureWarmRuntime: ensureOpenHandsWarmRuntime,
|
|
158
|
+
diagnosticChecks: openHandsDiagnosticChecks,
|
|
159
|
+
warmupProbeCommand: null,
|
|
160
|
+
taskExecute: executeWithOpenHands,
|
|
161
|
+
};
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenHands specialized task executor.
|
|
3
|
+
*
|
|
4
|
+
* Provides streaming line-by-line processing with stuck-guard detection,
|
|
5
|
+
* auto-steering nudges, activity-based timeout extension, and
|
|
6
|
+
* clarification/no-change detection.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from "fs";
|
|
10
|
+
import { resolve } from "path";
|
|
11
|
+
import type { JobResult } from "../common/types.js";
|
|
12
|
+
import type { WorkerpalsRuntimeConfig } from "../common/executor_backend.js";
|
|
13
|
+
import {
|
|
14
|
+
truncate,
|
|
15
|
+
streamLines,
|
|
16
|
+
parseStructuredResult,
|
|
17
|
+
filterResultLines,
|
|
18
|
+
} from "../common/execution_utils.js";
|
|
19
|
+
import { computeTimeoutWarningWindow } from "../timeout_policy.js";
|
|
20
|
+
|
|
21
|
+
// ---- Script path (resolved relative to this file) ----------------------------
|
|
22
|
+
|
|
23
|
+
const OPENHANDS_SCRIPT_PATH = resolve(import.meta.dir, "openhands", "openhands_executor.py");
|
|
24
|
+
|
|
25
|
+
// ---- OpenHands-specific helpers ----------------------------------------------
|
|
26
|
+
|
|
27
|
+
function classifyShellCommand(cmd: string): "explore" | "progress" {
|
|
28
|
+
const trimmed = cmd.trim().toLowerCase();
|
|
29
|
+
if (!trimmed) return "explore";
|
|
30
|
+
const token = trimmed.split(/\s+/, 1)[0] ?? "";
|
|
31
|
+
if (
|
|
32
|
+
token === "ls" ||
|
|
33
|
+
token === "find" ||
|
|
34
|
+
token === "rg" ||
|
|
35
|
+
token === "grep" ||
|
|
36
|
+
token === "cat" ||
|
|
37
|
+
token === "head" ||
|
|
38
|
+
token === "tail" ||
|
|
39
|
+
token === "sed" ||
|
|
40
|
+
token === "awk"
|
|
41
|
+
) {
|
|
42
|
+
return "explore";
|
|
43
|
+
}
|
|
44
|
+
if (token === "git") {
|
|
45
|
+
if (
|
|
46
|
+
/\bgit\s+(status|log|show|diff|branch|rev-parse|ls-files)\b/.test(trimmed) ||
|
|
47
|
+
/\bgit\s+grep\b/.test(trimmed)
|
|
48
|
+
) {
|
|
49
|
+
return "explore";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return "progress";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function classifyFileEditorSummary(line: string): "explore" | "progress" | null {
|
|
56
|
+
const lowered = line.toLowerCase();
|
|
57
|
+
if (!lowered.startsWith("summary: file_editor")) return null;
|
|
58
|
+
if (
|
|
59
|
+
lowered.includes('"command": "view"') ||
|
|
60
|
+
lowered.includes('"command":"view"') ||
|
|
61
|
+
lowered.includes('"command": "list"') ||
|
|
62
|
+
lowered.includes('"command":"list"')
|
|
63
|
+
) {
|
|
64
|
+
return "explore";
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
lowered.includes('"command": "create"') ||
|
|
68
|
+
lowered.includes('"command":"create"') ||
|
|
69
|
+
lowered.includes('"command": "str_replace"') ||
|
|
70
|
+
lowered.includes('"command":"str_replace"') ||
|
|
71
|
+
lowered.includes('"command": "insert"') ||
|
|
72
|
+
lowered.includes('"command":"insert"') ||
|
|
73
|
+
lowered.includes('"command": "delete"') ||
|
|
74
|
+
lowered.includes('"command":"delete"')
|
|
75
|
+
) {
|
|
76
|
+
return "progress";
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const OPENHANDS_NO_CHANGE_SIGNAL = ["no file changes detected", "no modified files were detected"];
|
|
82
|
+
|
|
83
|
+
const CLARIFICATION_SIGNAL_REGEX =
|
|
84
|
+
/\b(clarif(?:y|ication)|need to know which|could you clarify|please clarify|which .* would you like|let me ask for clarification)\b/i;
|
|
85
|
+
|
|
86
|
+
const NON_AGENT_LOG_LINE_REGEX =
|
|
87
|
+
/^(message from user|requested task:|tokens:|summary:|observation|tool:|result:|\$ )/i;
|
|
88
|
+
|
|
89
|
+
function hasOpenHandsNoChangeSignal(text: string): boolean {
|
|
90
|
+
const lowered = text.toLowerCase();
|
|
91
|
+
return OPENHANDS_NO_CHANGE_SIGNAL.some((token) => lowered.includes(token));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeAgentOutputLine(line: string): string {
|
|
95
|
+
return line
|
|
96
|
+
.replace(/^\[[^\]]+\]\s*/g, "")
|
|
97
|
+
.replace(/<\/?think>/gi, " ")
|
|
98
|
+
.replace(/```+/g, " ")
|
|
99
|
+
.replace(/\s+/g, " ")
|
|
100
|
+
.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function extractClarificationQuestionFromOutput(output: string): string | null {
|
|
104
|
+
if (!output.trim()) return null;
|
|
105
|
+
|
|
106
|
+
const rawLines = output
|
|
107
|
+
.split(/\r?\n/)
|
|
108
|
+
.map((line) => line.trim())
|
|
109
|
+
.filter(Boolean);
|
|
110
|
+
if (rawLines.length === 0) return null;
|
|
111
|
+
|
|
112
|
+
const markerIndex = rawLines.findIndex((line) => /message from agent/i.test(line));
|
|
113
|
+
const scopedLines = markerIndex >= 0 ? rawLines.slice(markerIndex + 1) : rawLines;
|
|
114
|
+
const lines = scopedLines
|
|
115
|
+
.map(normalizeAgentOutputLine)
|
|
116
|
+
.filter((line) => Boolean(line) && !NON_AGENT_LOG_LINE_REGEX.test(line));
|
|
117
|
+
if (lines.length === 0) return null;
|
|
118
|
+
|
|
119
|
+
const joined = lines.join("\n");
|
|
120
|
+
if (!CLARIFICATION_SIGNAL_REGEX.test(joined)) return null;
|
|
121
|
+
|
|
122
|
+
const explicitQuestion = [...lines].reverse().find((line) => line.includes("?"));
|
|
123
|
+
if (explicitQuestion) return explicitQuestion.slice(0, 280);
|
|
124
|
+
|
|
125
|
+
const fallback = [...lines].reverse().find((line) => CLARIFICATION_SIGNAL_REGEX.test(line));
|
|
126
|
+
return fallback ? fallback.slice(0, 280) : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- Main executor -----------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
export async function executeWithOpenHands(
|
|
132
|
+
kind: string,
|
|
133
|
+
params: Record<string, unknown>,
|
|
134
|
+
repo: string,
|
|
135
|
+
runtimeConfig: WorkerpalsRuntimeConfig,
|
|
136
|
+
onLog?: (stream: "stdout" | "stderr", line: string) => void,
|
|
137
|
+
budgets?: { executionBudgetMs?: number; finalizationBudgetMs?: number },
|
|
138
|
+
): Promise<JobResult> {
|
|
139
|
+
const pythonBin = runtimeConfig.workerpals.openhandsPython || "python";
|
|
140
|
+
const scriptPath = OPENHANDS_SCRIPT_PATH;
|
|
141
|
+
if (!existsSync(scriptPath)) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
summary: `OpenHands wrapper script not found: ${scriptPath}`,
|
|
145
|
+
exitCode: 1,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const configuredTimeoutMs = Math.max(10_000, runtimeConfig.workerpals.openhandsTimeoutMs);
|
|
150
|
+
const executionBudgetMs =
|
|
151
|
+
typeof budgets?.executionBudgetMs === "number" && Number.isFinite(budgets.executionBudgetMs)
|
|
152
|
+
? Math.max(10_000, Math.floor(budgets.executionBudgetMs))
|
|
153
|
+
: null;
|
|
154
|
+
const timeoutMs =
|
|
155
|
+
executionBudgetMs != null
|
|
156
|
+
? Math.min(configuredTimeoutMs, executionBudgetMs)
|
|
157
|
+
: configuredTimeoutMs;
|
|
158
|
+
const timeoutLimitSource =
|
|
159
|
+
executionBudgetMs == null
|
|
160
|
+
? `workerpals.openhands_timeout_ms=${configuredTimeoutMs}ms`
|
|
161
|
+
: executionBudgetMs < configuredTimeoutMs
|
|
162
|
+
? `planning executionBudgetMs=${executionBudgetMs}ms (worker cap=${configuredTimeoutMs}ms)`
|
|
163
|
+
: executionBudgetMs > configuredTimeoutMs
|
|
164
|
+
? `workerpals.openhands_timeout_ms=${configuredTimeoutMs}ms (planning executionBudgetMs=${executionBudgetMs}ms)`
|
|
165
|
+
: `planning executionBudgetMs=${executionBudgetMs}ms (matches worker cap)`;
|
|
166
|
+
if (executionBudgetMs != null && executionBudgetMs < configuredTimeoutMs) {
|
|
167
|
+
onLog?.(
|
|
168
|
+
"stdout",
|
|
169
|
+
`[OpenHandsExecutor] Capping execution timeout to ${timeoutMs}ms (planning executionBudgetMs=${executionBudgetMs}ms, worker cap=${configuredTimeoutMs}ms).`,
|
|
170
|
+
);
|
|
171
|
+
} else if (executionBudgetMs != null && executionBudgetMs > configuredTimeoutMs) {
|
|
172
|
+
onLog?.(
|
|
173
|
+
"stdout",
|
|
174
|
+
`[OpenHandsExecutor] Capping execution timeout to ${timeoutMs}ms (planning executionBudgetMs=${executionBudgetMs}ms, configured cap=${configuredTimeoutMs}ms).`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const { leadMs: timeoutWarningLeadMs, delayMs: timeoutWarningDelayMs } =
|
|
178
|
+
computeTimeoutWarningWindow(timeoutMs);
|
|
179
|
+
const finalizationBudgetMs =
|
|
180
|
+
typeof budgets?.finalizationBudgetMs === "number" &&
|
|
181
|
+
Number.isFinite(budgets.finalizationBudgetMs)
|
|
182
|
+
? Math.max(10_000, Math.floor(budgets.finalizationBudgetMs))
|
|
183
|
+
: 0;
|
|
184
|
+
const activityExtensionMs = Math.min(finalizationBudgetMs, 10 * 60_000);
|
|
185
|
+
const activityWindowMs = 90_000;
|
|
186
|
+
const payload = Buffer.from(
|
|
187
|
+
JSON.stringify({
|
|
188
|
+
kind,
|
|
189
|
+
params,
|
|
190
|
+
repo,
|
|
191
|
+
timeoutMs,
|
|
192
|
+
executionBudgetMs: executionBudgetMs ?? undefined,
|
|
193
|
+
finalizationBudgetMs: finalizationBudgetMs > 0 ? finalizationBudgetMs : undefined,
|
|
194
|
+
}),
|
|
195
|
+
"utf-8",
|
|
196
|
+
).toString("base64");
|
|
197
|
+
|
|
198
|
+
let warningTimer: ReturnType<typeof setTimeout> | null = null;
|
|
199
|
+
let timeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
200
|
+
let stuckNudgeStartTimer: ReturnType<typeof setTimeout> | null = null;
|
|
201
|
+
let stuckNudgeTimer: ReturnType<typeof setInterval> | null = null;
|
|
202
|
+
const outputPolicy = {
|
|
203
|
+
maxOutputChars: runtimeConfig.workerpals.outputMaxChars,
|
|
204
|
+
maxOutputLines: runtimeConfig.workerpals.outputMaxLines,
|
|
205
|
+
maxOutputHeadLines: runtimeConfig.workerpals.outputMaxHeadLines,
|
|
206
|
+
executorResultPrefix: runtimeConfig.workerpals.executorResultPrefix,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const proc = Bun.spawn([pythonBin, scriptPath, payload], {
|
|
211
|
+
cwd: repo,
|
|
212
|
+
stdout: "pipe",
|
|
213
|
+
stderr: "pipe",
|
|
214
|
+
env: {
|
|
215
|
+
...process.env,
|
|
216
|
+
PUSHPALS_REPO_PATH: repo,
|
|
217
|
+
PUSHPALS_ASSIGNED_REPO_ROOT: repo,
|
|
218
|
+
PYTHONIOENCODING: "utf-8",
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
let timedOut = false;
|
|
223
|
+
const startedAtMs = Date.now();
|
|
224
|
+
let lastActivityAtMs = startedAtMs;
|
|
225
|
+
let timeoutDeadlineMs = startedAtMs + timeoutMs;
|
|
226
|
+
let extendedByActivityMs = 0;
|
|
227
|
+
let timedOutAfterMs = timeoutMs;
|
|
228
|
+
const stuckGuardEnabled = runtimeConfig.workerpals.openhandsStuckGuardEnabled;
|
|
229
|
+
const stuckExploreLimit = runtimeConfig.workerpals.openhandsStuckGuardExploreLimit;
|
|
230
|
+
const stuckMinElapsedMs = runtimeConfig.workerpals.openhandsStuckGuardMinElapsedMs;
|
|
231
|
+
const stuckBroadScanLimit = runtimeConfig.workerpals.openhandsStuckGuardBroadScanLimit;
|
|
232
|
+
const stuckNoProgressMaxMs = runtimeConfig.workerpals.openhandsStuckGuardNoProgressMaxMs;
|
|
233
|
+
const stuckNudgeEnabled = runtimeConfig.workerpals.openhandsAutoSteerEnabled;
|
|
234
|
+
const stuckNudgeInitialDelayMs = Math.max(
|
|
235
|
+
0,
|
|
236
|
+
Math.floor(runtimeConfig.workerpals.openhandsAutoSteerInitialDelaySec * 1000),
|
|
237
|
+
);
|
|
238
|
+
const stuckNudgeIntervalMs = Math.max(
|
|
239
|
+
5_000,
|
|
240
|
+
Math.floor(runtimeConfig.workerpals.openhandsAutoSteerIntervalSec * 1000),
|
|
241
|
+
);
|
|
242
|
+
const stuckNudgeMaxCount = Math.max(0, runtimeConfig.workerpals.openhandsAutoSteerMaxNudges);
|
|
243
|
+
let exploreOps = 0;
|
|
244
|
+
let progressOps = 0;
|
|
245
|
+
let broadRepoScans = 0;
|
|
246
|
+
let stuckGuardTriggered = false;
|
|
247
|
+
let stuckGuardReason = "";
|
|
248
|
+
let stuckGuardAfterMs = 0;
|
|
249
|
+
let stuckNudgeCount = 0;
|
|
250
|
+
|
|
251
|
+
const stopStuckNudges = (reason?: string) => {
|
|
252
|
+
const hadActiveTimer = Boolean(stuckNudgeStartTimer || stuckNudgeTimer);
|
|
253
|
+
if (stuckNudgeStartTimer) {
|
|
254
|
+
clearTimeout(stuckNudgeStartTimer);
|
|
255
|
+
stuckNudgeStartTimer = null;
|
|
256
|
+
}
|
|
257
|
+
if (stuckNudgeTimer) {
|
|
258
|
+
clearInterval(stuckNudgeTimer);
|
|
259
|
+
stuckNudgeTimer = null;
|
|
260
|
+
}
|
|
261
|
+
if (reason && hadActiveTimer) {
|
|
262
|
+
onLog?.("stdout", `[OpenHandsExecutor] Auto-steering nudges paused: ${reason}.`);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const buildSteeringNudge = (nudgeIndex: number): string => {
|
|
267
|
+
if (nudgeIndex === 1) {
|
|
268
|
+
return (
|
|
269
|
+
"Auto-steering nudge 1: stop broad exploration and lock onto one concrete target file. " +
|
|
270
|
+
"Make one minimal edit and run one focused validation command."
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (nudgeIndex === 2) {
|
|
274
|
+
return (
|
|
275
|
+
"Auto-steering nudge 2: choose the best candidate file now, apply a small correct patch, " +
|
|
276
|
+
"then run a narrow test/lint command for that change."
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return (
|
|
280
|
+
"Auto-steering nudge: if still blocked, stop scanning loops and return concise blocker status " +
|
|
281
|
+
"with the next concrete command you would run."
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const startStuckNudges = () => {
|
|
286
|
+
if (!stuckNudgeEnabled || stuckNudgeMaxCount <= 0) return;
|
|
287
|
+
if (stuckNudgeStartTimer || stuckNudgeTimer) return;
|
|
288
|
+
|
|
289
|
+
const emitNudge = () => {
|
|
290
|
+
if (timedOut) {
|
|
291
|
+
stopStuckNudges();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (progressOps > 0) {
|
|
295
|
+
stopStuckNudges("progress detected");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
stuckNudgeCount += 1;
|
|
299
|
+
const elapsedMs = Date.now() - startedAtMs;
|
|
300
|
+
onLog?.(
|
|
301
|
+
"stdout",
|
|
302
|
+
`[OpenHandsExecutor] Auto-steering nudge ${stuckNudgeCount}/${stuckNudgeMaxCount} after ${elapsedMs}ms (${stuckGuardReason || "no edit/test progress"}): ${buildSteeringNudge(stuckNudgeCount)}`,
|
|
303
|
+
);
|
|
304
|
+
if (stuckNudgeCount >= stuckNudgeMaxCount) {
|
|
305
|
+
stopStuckNudges();
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const startInterval = () => {
|
|
310
|
+
if (stuckNudgeTimer || stuckNudgeCount >= stuckNudgeMaxCount) return;
|
|
311
|
+
stuckNudgeTimer = setInterval(emitNudge, stuckNudgeIntervalMs);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
if (stuckNudgeInitialDelayMs <= 0) {
|
|
315
|
+
emitNudge();
|
|
316
|
+
startInterval();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
stuckNudgeStartTimer = setTimeout(() => {
|
|
321
|
+
stuckNudgeStartTimer = null;
|
|
322
|
+
emitNudge();
|
|
323
|
+
startInterval();
|
|
324
|
+
}, stuckNudgeInitialDelayMs);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const onProcessLine = (stream: "stdout" | "stderr", line: string) => {
|
|
328
|
+
lastActivityAtMs = Date.now();
|
|
329
|
+
const trimmed = line.trim();
|
|
330
|
+
if (trimmed.startsWith("$ ")) {
|
|
331
|
+
const commandText = trimmed.slice(2).trim();
|
|
332
|
+
if (classifyShellCommand(commandText) === "explore") {
|
|
333
|
+
exploreOps += 1;
|
|
334
|
+
} else {
|
|
335
|
+
progressOps += 1;
|
|
336
|
+
}
|
|
337
|
+
const lowered = commandText.toLowerCase();
|
|
338
|
+
if (/\bfind\s+\/repo\b/.test(lowered) || /\bfind\s+\/\b/.test(lowered)) {
|
|
339
|
+
broadRepoScans += 1;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const fileEditorClass = classifyFileEditorSummary(trimmed);
|
|
343
|
+
if (fileEditorClass === "explore") exploreOps += 1;
|
|
344
|
+
if (fileEditorClass === "progress") progressOps += 1;
|
|
345
|
+
if (stuckGuardTriggered && progressOps > 0) {
|
|
346
|
+
stopStuckNudges("progress detected");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!stuckGuardTriggered && stuckGuardEnabled && progressOps === 0) {
|
|
350
|
+
const elapsedMs = Date.now() - startedAtMs;
|
|
351
|
+
const noProgressTooLong = elapsedMs >= stuckNoProgressMaxMs;
|
|
352
|
+
const tooManyExplores = elapsedMs >= stuckMinElapsedMs && exploreOps >= stuckExploreLimit;
|
|
353
|
+
const tooManyBroadScans = broadRepoScans >= stuckBroadScanLimit;
|
|
354
|
+
if (noProgressTooLong || tooManyExplores || tooManyBroadScans) {
|
|
355
|
+
stuckGuardTriggered = true;
|
|
356
|
+
stuckGuardAfterMs = elapsedMs;
|
|
357
|
+
if (tooManyBroadScans) {
|
|
358
|
+
stuckGuardReason = `repeated broad filesystem scans (count=${broadRepoScans}) with no edits/tests`;
|
|
359
|
+
} else if (tooManyExplores) {
|
|
360
|
+
stuckGuardReason = `repeated exploratory actions (count=${exploreOps}) with no edits/tests`;
|
|
361
|
+
} else {
|
|
362
|
+
stuckGuardReason = `no edit/test progress for ${stuckNoProgressMaxMs}ms`;
|
|
363
|
+
}
|
|
364
|
+
onLog?.(
|
|
365
|
+
"stdout",
|
|
366
|
+
`[OpenHandsExecutor] Stuck guard triggered after ${stuckGuardAfterMs}ms: ${stuckGuardReason}. Steering hint: stop broad exploration, pick a concrete target file, make a minimal edit, then run a focused validation command.`,
|
|
367
|
+
);
|
|
368
|
+
startStuckNudges();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
onLog?.(stream, line);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const resetWarningTimer = () => {
|
|
375
|
+
if (warningTimer) {
|
|
376
|
+
clearTimeout(warningTimer);
|
|
377
|
+
warningTimer = null;
|
|
378
|
+
}
|
|
379
|
+
const msUntilWarn = timeoutDeadlineMs - Date.now() - timeoutWarningLeadMs;
|
|
380
|
+
if (msUntilWarn <= 0) return;
|
|
381
|
+
warningTimer = setTimeout(() => {
|
|
382
|
+
onLog?.(
|
|
383
|
+
"stdout",
|
|
384
|
+
`[OpenHandsExecutor] Timeout approaching for ${kind} (${Math.round(
|
|
385
|
+
timeoutWarningLeadMs / 1000,
|
|
386
|
+
)}s remaining). If unfinished, return a concise status/failure update now.`,
|
|
387
|
+
);
|
|
388
|
+
}, msUntilWarn);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const resetTimeoutTimer = () => {
|
|
392
|
+
if (timeoutTimer) {
|
|
393
|
+
clearTimeout(timeoutTimer);
|
|
394
|
+
timeoutTimer = null;
|
|
395
|
+
}
|
|
396
|
+
const msUntilTimeout = Math.max(1, timeoutDeadlineMs - Date.now());
|
|
397
|
+
timeoutTimer = setTimeout(() => {
|
|
398
|
+
const nowMs = Date.now();
|
|
399
|
+
const quietForMs = nowMs - lastActivityAtMs;
|
|
400
|
+
if (
|
|
401
|
+
extendedByActivityMs === 0 &&
|
|
402
|
+
activityExtensionMs > 0 &&
|
|
403
|
+
quietForMs <= activityWindowMs
|
|
404
|
+
) {
|
|
405
|
+
extendedByActivityMs = activityExtensionMs;
|
|
406
|
+
timeoutDeadlineMs = nowMs + activityExtensionMs;
|
|
407
|
+
onLog?.(
|
|
408
|
+
"stdout",
|
|
409
|
+
`[OpenHandsExecutor] Extending timeout by ${activityExtensionMs}ms because the agent is still active (last output ${Math.round(
|
|
410
|
+
quietForMs / 1000,
|
|
411
|
+
)}s ago).`,
|
|
412
|
+
);
|
|
413
|
+
resetWarningTimer();
|
|
414
|
+
resetTimeoutTimer();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
timedOut = true;
|
|
419
|
+
timedOutAfterMs = Math.max(1, nowMs - startedAtMs);
|
|
420
|
+
onLog?.(
|
|
421
|
+
"stdout",
|
|
422
|
+
`[OpenHandsExecutor] Timeout reached for ${kind} after ${timedOutAfterMs}ms (effective limit: ${timeoutLimitSource}${
|
|
423
|
+
extendedByActivityMs > 0 ? ` + activity extension ${extendedByActivityMs}ms` : ""
|
|
424
|
+
}); terminating wrapper process.`,
|
|
425
|
+
);
|
|
426
|
+
stopStuckNudges();
|
|
427
|
+
try {
|
|
428
|
+
proc.kill();
|
|
429
|
+
} catch (_e) {}
|
|
430
|
+
}, msUntilTimeout);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
resetWarningTimer();
|
|
434
|
+
resetTimeoutTimer();
|
|
435
|
+
|
|
436
|
+
const [stdout, stderr] = await Promise.all([
|
|
437
|
+
streamLines(proc.stdout, "stdout", onProcessLine),
|
|
438
|
+
streamLines(proc.stderr, "stderr", onProcessLine),
|
|
439
|
+
]);
|
|
440
|
+
if (warningTimer) {
|
|
441
|
+
clearTimeout(warningTimer);
|
|
442
|
+
warningTimer = null;
|
|
443
|
+
}
|
|
444
|
+
if (timeoutTimer) {
|
|
445
|
+
clearTimeout(timeoutTimer);
|
|
446
|
+
timeoutTimer = null;
|
|
447
|
+
}
|
|
448
|
+
stopStuckNudges();
|
|
449
|
+
const exitCode = await proc.exited;
|
|
450
|
+
|
|
451
|
+
const parsed = parseStructuredResult(stdout, outputPolicy.executorResultPrefix);
|
|
452
|
+
const filteredStdout = filterResultLines(stdout, outputPolicy.executorResultPrefix);
|
|
453
|
+
|
|
454
|
+
if (!parsed) {
|
|
455
|
+
if (timedOut) {
|
|
456
|
+
const stuckNote = stuckGuardTriggered
|
|
457
|
+
? ` Stuck guard warning was raised at ${stuckGuardAfterMs}ms (${stuckGuardReason}).`
|
|
458
|
+
: "";
|
|
459
|
+
return {
|
|
460
|
+
ok: false,
|
|
461
|
+
summary: `OpenHands wrapper timed out after ${timedOutAfterMs}ms for ${kind} (effective limit: ${timeoutLimitSource}${
|
|
462
|
+
extendedByActivityMs > 0 ? ` + activity extension ${extendedByActivityMs}ms` : ""
|
|
463
|
+
}). Worker returned a timeout failure.${stuckNote}`,
|
|
464
|
+
stdout: truncate(filteredStdout, outputPolicy),
|
|
465
|
+
stderr: truncate(stderr, outputPolicy),
|
|
466
|
+
exitCode: exitCode === 0 ? 124 : exitCode,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
ok: false,
|
|
471
|
+
summary: `OpenHands wrapper did not return a structured result for ${kind}`,
|
|
472
|
+
stdout: truncate(filteredStdout, outputPolicy),
|
|
473
|
+
stderr: truncate(stderr, outputPolicy),
|
|
474
|
+
exitCode,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const summary =
|
|
479
|
+
typeof parsed.summary === "string"
|
|
480
|
+
? parsed.summary
|
|
481
|
+
: exitCode === 0
|
|
482
|
+
? `${kind} passed via OpenHands`
|
|
483
|
+
: `${kind} failed via OpenHands (exit ${exitCode})`;
|
|
484
|
+
const parsedStdout = typeof parsed.stdout === "string" ? parsed.stdout : filteredStdout;
|
|
485
|
+
const parsedStderr = typeof parsed.stderr === "string" ? parsed.stderr : stderr;
|
|
486
|
+
const parsedExitCode =
|
|
487
|
+
typeof parsed.exitCode === "number" && Number.isFinite(parsed.exitCode)
|
|
488
|
+
? parsed.exitCode
|
|
489
|
+
: exitCode;
|
|
490
|
+
const parsedOk = typeof parsed.ok === "boolean" ? parsed.ok : parsedExitCode === 0;
|
|
491
|
+
const noChangeResult =
|
|
492
|
+
parsedOk &&
|
|
493
|
+
(hasOpenHandsNoChangeSignal(summary) ||
|
|
494
|
+
hasOpenHandsNoChangeSignal(String(parsedStdout ?? "")) ||
|
|
495
|
+
hasOpenHandsNoChangeSignal(String(parsedStderr ?? "")));
|
|
496
|
+
if (noChangeResult) {
|
|
497
|
+
const clarificationQuestion = extractClarificationQuestionFromOutput(filteredStdout);
|
|
498
|
+
if (clarificationQuestion) {
|
|
499
|
+
return {
|
|
500
|
+
ok: true,
|
|
501
|
+
summary: `OpenHands needs clarification: ${clarificationQuestion}`,
|
|
502
|
+
stdout: truncate(filteredStdout || String(parsedStdout ?? ""), outputPolicy),
|
|
503
|
+
stderr: truncate(`Clarification needed: ${clarificationQuestion}`, outputPolicy),
|
|
504
|
+
exitCode: 0,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
ok: parsedOk,
|
|
511
|
+
summary,
|
|
512
|
+
stdout: truncate(parsedStdout ?? "", outputPolicy),
|
|
513
|
+
stderr: truncate(parsedStderr ?? "", outputPolicy),
|
|
514
|
+
exitCode: parsedExitCode,
|
|
515
|
+
};
|
|
516
|
+
} catch (err) {
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
summary: `OpenHands wrapper execution error for ${kind}: ${String(err)}`,
|
|
520
|
+
exitCode: 1,
|
|
521
|
+
};
|
|
522
|
+
} finally {
|
|
523
|
+
if (warningTimer) {
|
|
524
|
+
clearTimeout(warningTimer);
|
|
525
|
+
}
|
|
526
|
+
if (timeoutTimer) {
|
|
527
|
+
clearTimeout(timeoutTimer);
|
|
528
|
+
}
|
|
529
|
+
if (stuckNudgeStartTimer) {
|
|
530
|
+
clearTimeout(stuckNudgeStartTimer);
|
|
531
|
+
}
|
|
532
|
+
if (stuckNudgeTimer) {
|
|
533
|
+
clearInterval(stuckNudgeTimer);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|