-
Notifications
You must be signed in to change notification settings - Fork 654
Add a built-in Cline coding-agent harness #2431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
arafatkatze
wants to merge
8
commits into
PrimeIntellect-ai:main
Choose a base branch
from
arafatkatze:cline-harness
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 7 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
99e4322
feat: add built-in Cline harness
arafatkatze 0dace07
test: cover Cline in agentic e2e matrix
arafatkatze 58f6efc
docs: document Cline harness usage and limits
arafatkatze 87dcc41
test: verify Cline harness launch plumbing
arafatkatze 940b05a
test: rely on v1 Cline e2e coverage
arafatkatze 4268422
fix(cline): preserve native tool defaults
arafatkatze 6650884
feat(cline): use native ACP harness
arafatkatze d539ef0
fix(cline): address harness review feedback
arafatkatze File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import json | ||
| from types import SimpleNamespace | ||
| from typing import cast | ||
| from unittest.mock import AsyncMock | ||
|
|
||
| import pytest | ||
|
|
||
| from verifiers.v1.clients import ModelContext | ||
| from verifiers.v1.harnesses.cline.harness import ClineHarness, ClineHarnessConfig | ||
| from verifiers.v1.runtimes import ProgramResult | ||
| from verifiers.v1.task import TaskData | ||
| from verifiers.v1.trace import Trace | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_prepare_acp_allows_user_to_open_conversation(): | ||
| harness = ClineHarness(ClineHarnessConfig(id="cline")) | ||
| runtime = AsyncMock() | ||
| runtime.run.return_value = ProgramResult(exit_code=0, stdout="", stderr="") | ||
|
|
||
| config = await harness.prepare_acp( | ||
| cast(ModelContext, SimpleNamespace(model="test/model")), | ||
| cast(Trace, SimpleNamespace(id="trace-id")), | ||
| runtime, | ||
| "http://model.invalid/v1", | ||
| "interception-secret", | ||
| {"resume": "http://tool.invalid/mcp"}, | ||
| TaskData(prompt=None, system_prompt="system"), | ||
| ) | ||
|
|
||
| assert config.prompt is None | ||
| assert config.system_prompt == "system" | ||
| assert "--acp" in config.command | ||
| assert config.mcp_urls == {} | ||
| mcp_payload = next( | ||
| call.args[1] | ||
| for call in runtime.write.await_args_list | ||
| if call.args[0].endswith("cline_mcp_settings.json") | ||
| ) | ||
| assert json.loads(mcp_payload) == { | ||
| "mcpServers": { | ||
| "resume": { | ||
| "transport": { | ||
| "type": "streamableHttp", | ||
| "url": "http://tool.invalid/mcp", | ||
| } | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Cline harness | ||
|
|
||
| The `cline` harness runs the public Cline CLI through its native ACP server and | ||
| routes every model call through the rollout's interception server. Cline's tool | ||
| calls, results, usage, and final responses become ordinary verifiers trace nodes | ||
| and model-call records while one native session persists across resumed turns. | ||
|
|
||
| ## Quickstart | ||
|
|
||
| Choose an isolated container or VM runtime. The harness installs its pinned Cline | ||
| release during trusted setup. | ||
|
|
||
| ```bash | ||
| export OPENAI_API_KEY=... | ||
| uv run eval MY_TASKSET \ | ||
| --env.agent.harness.id cline \ | ||
| --env.agent.runtime.type docker \ | ||
| --model openai/gpt-4.1-mini \ | ||
| --client.base-url https://openrouter.ai/api/v1 \ | ||
| --client.api-key-var OPENAI_API_KEY \ | ||
| --no-push | ||
| ``` | ||
|
|
||
| `--no-push` keeps the run local. Inspect the resolved config and traces under | ||
| `outputs/<run>/configs/eval.json` and `outputs/<run>/traces.jsonl`. | ||
|
|
||
| ## Harness configuration | ||
|
|
||
| ```toml | ||
| [env.agent.harness] | ||
| id = "cline" | ||
| version = "3.0.57" | ||
| disabled_tools = [] # native Cline tool names to disable | ||
| ``` | ||
|
|
||
| Use the agent and sampling blocks for settings owned by verifiers: | ||
|
|
||
| ```toml | ||
| [env.agent] | ||
| max_turns = 20 | ||
| max_output_tokens = 32768 | ||
|
|
||
| [env.agent.timeout] | ||
| setup = 600 | ||
| rollout = 1800 | ||
|
|
||
| [sampling] | ||
| temperature = 0.2 | ||
| ``` | ||
|
|
||
| The interception server applies the configured model and sampling values to | ||
| Cline's requests before forwarding them upstream. | ||
|
|
||
| ## Security posture and limitations | ||
|
|
||
| - Each rollout gets an isolated Cline data directory, deleted after scoring. | ||
| - Cline telemetry and update checks are disabled. | ||
| - Per-rollout MCP servers are loaded through Cline's native MCP settings file. | ||
| - Native Cline tools remain enabled unless they are listed in `disabled_tools`. | ||
| - The adapter keeps one native ACP session alive across env-driven user turns. | ||
| - Cline exposes synchronous `tool_call` and `tool_result` hooks, but this harness | ||
| does not advertise verifiers tool interception because the public hook control | ||
| surface does not preserve the full result-rewrite contract. | ||
| - Cline `3.0.57` has no max-step flag. Use verifiers' `max_turns`, token caps, and | ||
| rollout timeout; their stop conditions and errors are recorded on the trace. | ||
| - Eval-client traces preserve message/tool structure and provider-reported usage, | ||
| but exact token IDs and masks require a compatible training client/renderer. | ||
| - Compaction was not forced in the smoke validation. If Cline rewrites history, | ||
| inspect the resulting branches before using those samples for training. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from verifiers.v1.harnesses.cline.harness import ClineHarness, ClineHarnessConfig | ||
|
|
||
| __all__ = ["ClineHarness", "ClineHarnessConfig"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| """Run the public Cline CLI through its native ACP server.""" | ||
|
|
||
| import json | ||
| import logging | ||
| import shlex | ||
|
|
||
| from pydantic import Field | ||
|
|
||
| from verifiers.v1.acp import ACPConfig, ACPHarness | ||
| from verifiers.v1.clients import ModelContext | ||
| from verifiers.v1.configs.harness import HarnessConfig | ||
| from verifiers.v1.harnesses.node import NODE_BIN_DIR, ensure_node | ||
| from verifiers.v1.runtimes import Runtime | ||
| from verifiers.v1.task import TaskData | ||
| from verifiers.v1.trace import Trace | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| CLINE_DIR = "/var/tmp/vf-cline-{version}" | ||
| PACKAGES_DIR = f"{CLINE_DIR}/packages" | ||
| # Cline's package publishes its own platform resolver. npm 11 does not create a | ||
| # `.bin/cline` shim for this package in the minimal prefix install, so address the | ||
| # declared binary directly. | ||
| CLINE_BIN = f"{PACKAGES_DIR}/node_modules/cline/bin/cline" | ||
| CLINE_DATA_ROOT = "/tmp/vf-cline" | ||
|
|
||
| INSTALL = r""" | ||
| set -e | ||
| export PATH="/var/tmp/vf-node/bin:$PATH" | ||
| rm -f {ready} | ||
| npm install --prefix {packages} --no-audit --no-fund --omit=dev \ | ||
| "cline@$VF_CLINE_VERSION" >/dev/null | ||
| touch {ready} | ||
| """ | ||
|
|
||
|
|
||
| class ClineHarnessConfig(HarnessConfig): | ||
| version: str = Field(default="3.0.57", pattern=r"^[A-Za-z0-9._+-]+$") | ||
| """Public Cline CLI release to install, pinned for reproducibility.""" | ||
|
|
||
|
|
||
| class ClineHarness(ACPHarness[ClineHarnessConfig]): | ||
| APPENDS_SYSTEM_PROMPT = True | ||
| SUPPORTS_MCP = True | ||
|
|
||
| async def setup(self, runtime: Runtime) -> None: | ||
| await ensure_node(runtime) | ||
| directory = CLINE_DIR.format(version=self.config.version) | ||
| packages = PACKAGES_DIR.format(version=self.config.version) | ||
| cline_bin = CLINE_BIN.format(version=self.config.version) | ||
| ready = f"{directory}/.ready" | ||
| script = INSTALL.replace("{packages}", packages).replace("{ready}", ready) | ||
| ensure = shlex.quote(f"[ -f {ready} ] && [ -x {cline_bin} ] || ({script})") | ||
| guarded = ( | ||
| f"mkdir -p {directory} && " | ||
| f'"$(command -v flock || command -v lockf)" {directory}/install.lock ' | ||
| f"sh -c {ensure}" | ||
| ) | ||
| logger.info("cline: ensuring Cline CLI %s is installed", self.config.version) | ||
| result = await runtime.run( | ||
| ["sh", "-c", guarded], | ||
| { | ||
| **self.config.resolved_env, | ||
| "VF_CLINE_VERSION": self.config.version, | ||
| }, | ||
| ) | ||
| if result.exit_code != 0: | ||
| detail = (result.stderr or result.stdout).strip()[-500:] | ||
| raise RuntimeError(f"Cline CLI install failed: {detail}") | ||
| await super().setup(runtime) | ||
|
|
||
| async def prepare_acp( | ||
| self, | ||
| ctx: ModelContext, | ||
| trace: Trace, | ||
| runtime: Runtime, | ||
| endpoint: str, | ||
| secret: str, | ||
| mcp_urls: dict[str, str], | ||
| data: TaskData, | ||
| ) -> ACPConfig: | ||
| system_prompt, prompt = self.resolve_prompt(data) | ||
|
|
||
| data_dir = self.data_dir(trace) | ||
| settings_dir = f"{data_dir}/settings" | ||
| mcp_settings = f"{settings_dir}/cline_mcp_settings.json" | ||
| await runtime.write( | ||
| f"{settings_dir}/global-settings.json", | ||
| json.dumps({"disabledTools": self.config.disabled_tools or []}).encode(), | ||
| ) | ||
| await runtime.write( | ||
| mcp_settings, | ||
| json.dumps( | ||
| { | ||
| "mcpServers": { | ||
| name: { | ||
| "transport": { | ||
|
arafatkatze marked this conversation as resolved.
|
||
| "type": "streamableHttp", | ||
| "url": url, | ||
| } | ||
| } | ||
| for name, url in mcp_urls.items() | ||
| } | ||
| } | ||
| ).encode(), | ||
| ) | ||
|
|
||
| env = { | ||
| **self.config.resolved_env, | ||
| "PATH": ( | ||
| f"{NODE_BIN_DIR}:/usr/local/sbin:/usr/local/bin:" | ||
| "/usr/sbin:/usr/bin:/sbin:/bin" | ||
| ), | ||
| "CLINE_DATA_DIR": data_dir, | ||
| "CLINE_MCP_SETTINGS_PATH": mcp_settings, | ||
| "CLINE_PROVIDER": "openai-compatible", | ||
|
arafatkatze marked this conversation as resolved.
|
||
| "CLINE_API_KEY": secret, | ||
| "CLINE_MODEL": ctx.model, | ||
| "CLINE_TELEMETRY_DISABLED": "1", | ||
|
arafatkatze marked this conversation as resolved.
Outdated
|
||
| "CLINE_NO_AUTO_UPDATE": "1", | ||
| "NO_UPDATE_NOTIFIER": "1", | ||
| } | ||
| # Execute npm's launcher through its shebang. Passing the `.bin` symlink | ||
| # to `node` directly bypasses the package launcher's intended resolution | ||
| # path for the platform-specific compiled binary. | ||
| cline = [CLINE_BIN.format(version=self.config.version)] | ||
| auth = await runtime.run( | ||
| [ | ||
| *cline, | ||
| "auth", | ||
| "--provider", | ||
| "openai-compatible", | ||
| "--apikey", | ||
| secret, | ||
| "--modelid", | ||
| ctx.model, | ||
| "--baseurl", | ||
| endpoint, | ||
| "--data-dir", | ||
| data_dir, | ||
| ], | ||
| env, | ||
| ) | ||
| if auth.exit_code != 0: | ||
| detail = (auth.stderr or auth.stdout).strip()[-500:] | ||
| raise RuntimeError(f"Cline provider configuration failed: {detail}") | ||
|
|
||
| return ACPConfig( | ||
| env=env, | ||
| command=[ | ||
| *cline, | ||
| "--acp", | ||
| "--auto-approve", | ||
| "true", | ||
| "--cwd", | ||
| ".", | ||
| "--data-dir", | ||
| data_dir, | ||
| ], | ||
| prompt=prompt, | ||
| # Cline reads task-scoped servers from CLINE_MCP_SETTINGS_PATH. | ||
| mcp_urls={}, | ||
| system_prompt=system_prompt, | ||
| ) | ||
|
|
||
| async def cleanup(self, trace: Trace, runtime: Runtime) -> None: | ||
| result = await runtime.run(["rm", "-rf", self.data_dir(trace)], {}) | ||
| if result.exit_code != 0: | ||
| detail = (result.stderr or result.stdout).strip()[-500:] | ||
| raise RuntimeError(f"failed to clean up Cline data: {detail}") | ||
|
|
||
| @staticmethod | ||
| def data_dir(trace: Trace) -> str: | ||
| return f"{CLINE_DATA_ROOT}/{trace.id}" | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.