Skip to content

feat(python-sdk): add CursorProvider to the harness - #1042

Closed
dchaudhari7177 wants to merge 3 commits into
Agent-Field:mainfrom
dchaudhari7177:feat/cursor-python-provider
Closed

dchaudhari7177 wants to merge 3 commits into
Agent-Field:mainfrom
dchaudhari7177:feat/cursor-python-provider

Conversation

@dchaudhari7177

Copy link
Copy Markdown

Closes #292. Part of #291.

Follows codex.py as the reference, per the last acceptance item: same RawResult/Metrics mapping, same failure-type classification, same env/cwd handling, same parse_jsonl / extract_final_text / extract_token_usage pipeline.

Four places I did not copy blindly

The binary is agent, not cursor. Cursor ships its headless agent under that name, so cursor_bin defaults to "agent" — the one provider whose default does not match its own name. There is a test pinning it, because that mismatch looks like a typo and is the sort of thing a later edit would helpfully "correct".

An unset permission_mode maps to --mode plan, not --mode ask. The issue's table says None--mode ask, and I think that is wrong in this context: ask waits for an interactive answer, and a subprocess has nobody to give one, so the run would hang until the harness timeout rather than fail. Planning is the safe reading of "no mode stated", and it fails fast. "auto"--force and "plan"--mode plan are as specified. Happy to change it if ask is deliberate.

CURSOR_API_KEY only when supplied. Setting it unconditionally would clobber an inherited key with an empty string. It goes through the environment, never argv — a test asserts the key does not appear in the command, since argv is visible in ps.

--resume omitted on an empty id. The first turn of a session has no chat id, and --resume "" is an error rather than a no-op.

Wiring

SUPPORTED_PROVIDERS, build_provider, HarnessConfig.cursor_bin, the provider field's description, and the _runner option passthrough.

Tests — 19, all passing

Basic execution and exact command construction; prompt-last positioning (a flag after it would be read as part of the prompt); session resume in both directions — --resume out, session_id back; the three permission modes, parametrised; api-key present and absent; binary-missing → HarnessProviderUnavailable; timeout → FailureType.TIMEOUT not CRASH; non-zero exit with output (not an error, matching codex) and without (error); kill-by-signal; malformed NDJSON lines skipped; cost estimation into metrics.

test_harness_factory.py, test_harness_provider_codex.py, test_harness_defaults.py and test_harness_packaging.py still pass — 56 total, nothing regressed.

ruff check clean on every file I touched. ruff format --check clean on the new files; agentfield/types.py reports as unformatted, but it does so on unmodified main too, so I left it rather than mixing an unrelated reformat into this diff.

Not in scope here

.agentfield_output.json schema validation is handled generically by the runner rather than per-provider, so there was nothing provider-specific to add — flagging that rather than silently ticking the box. If Cursor needs its own hook there, that is a separate change.

Adds "cursor" as a harness provider, following codex.py as the reference:
same RawResult/Metrics mapping, same failure-type classification, same env and
cwd handling.

Notes on the parts that are not a straight copy:

- The binary defaults to "agent", not "cursor". Cursor ships its headless
  agent under that name, so cursor_bin's default cannot follow the provider
  name the way codex_bin's and gemini_bin's do. A test pins it, because the
  mismatch is the sort of thing a later edit would quietly correct.

- An unset permission_mode maps to --mode plan rather than --mode ask. Ask
  waits for an interactive answer and a subprocess has nobody to give one, so
  the run would hang to the harness timeout instead of failing. "auto" maps to
  --force and "plan" to --mode plan as specified.

- CURSOR_API_KEY is set only when the caller supplies a key, so an inherited
  one is not clobbered with an empty string, and it goes through the
  environment rather than argv where it would be visible in ps.

- --resume is omitted when the session id is empty, since the first turn of a
  session has none and an empty value is an error.

Wired into SUPPORTED_PROVIDERS, build_provider, HarnessConfig.cursor_bin, the
provider-list description and the _runner passthrough.

19 tests cover command construction, prompt-last positioning, session resume in
both directions, the three permission modes, api-key handling, binary-missing,
timeout, non-zero exit with and without output, kill-by-signal, malformed
stream lines, and cost estimation.

Part of Agent-Field#291. Closes Agent-Field#292
@dchaudhari7177
dchaudhari7177 requested review from a team and AbirAbbas as code owners September 6, 2026 10:30
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@santoshkumarradha santoshkumarradha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good on my side. The provider follows the existing harness contract, keeps the API key out of argv, handles session resume and failure modes, and the focused provider suite plus Ruff pass locally. I also checked the command flags against the installed Cursor CLI.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Performance

SDK Memory Δ Latency Δ Tests Status
Python 9.0 KB - 0.30 µs -14%

✓ No regressions detected

@santoshkumarradha santoshkumarradha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One integration point is still missing. Adding cursor to SUPPORTED_PROVIDERS makes harness_doctor() index PROVIDER_SPECS["cursor"], but this PR does not add that spec. The full Python CI reproduces it as test_doctor_checks_all_supported_providers_with_no_args failing with KeyError: cursor. Please add the Cursor availability/doctor spec and cover the doctor report. The focused provider tests and Ruff are otherwise clean.

Adding "cursor" to SUPPORTED_PROVIDERS without a PROVIDER_SPECS entry made
harness_doctor() raise KeyError on the no-args path, which walks every
supported provider. That is what turned test_doctor_checks_all_supported_
providers_with_no_args red.

The spec is the one in the table whose binary does not match its provider
name: Cursor ships its headless CLI as `agent`, so the doctor has to probe
`agent --version`, not `cursor --version`. A comment says so, next to the
same note already on CursorProvider.bin_path.

Three tests: the doctor report for an installed cursor (asserting the probe
command is `agent`, not `cursor`), the missing-binary report and its install
command, and a standing check that every entry in SUPPORTED_PROVIDERS has
either a spec or the documented claude-code wrapper branch. The last one is
what catches the next provider added the same way.
@dchaudhari7177

Copy link
Copy Markdown
Author

Good catch — thank you, that was a real integration hole and the reproducer was exact.

PROVIDER_SPECS["cursor"] is added in c85837d. The detail worth flagging: it is the one spec whose binary does not match its provider name. Cursor ships its headless CLI as agent, so the doctor probes agent --version; specifying cursor there would have made harness_doctor() report the provider as not installed on a machine where it is. There's a comment on the spec pointing at the matching note on CursorProvider.bin_path so the two stay in step.

Three tests rather than one:

  1. test_doctor_probes_cursor_through_its_agent_binary — full ProviderHealth equality, and asserts the probe command is ["agent", "--version"].
  2. test_doctor_reports_a_missing_cursor_cli_with_its_install_command — the binary_not_found path.
  3. test_every_supported_provider_has_a_doctor_specSUPPORTED_PROVIDERS <= set(PROVIDER_SPECS) | {"claude-code"}. This is the one that matters beyond this PR: harness_doctor() indexes PROVIDER_SPECS directly, so the next provider added to the set without a spec is a runtime KeyError, not an import error. claude-code is the documented exception since it takes the _claude_health wrapper branch instead of a CLI probe.

Verification:

pytest tests/test_harness_doctor.py tests/test_harness_provider_cursor.py   28 passed
ruff check agentfield tests                                                 All checks passed
ruff format --check                                                         clean

The 7 test_harness_ai_schema_repair.py failures in my local run are present on main with this branch stashed, so they're not from this change.

On the CLA: it's showing not-signed on this PR — I'll get that sorted.

@santoshkumarradha santoshkumarradha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing the missing doctor integration. I rechecked the updated head: the Cursor provider and doctor tests pass locally, and Ruff is clean. The provider spec now probes the correct agent binary and the coverage guard will catch future provider/spec drift. This looks good once the CLA and CI are green.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage gate

Thresholds from .coverage-gate.toml: per-surface ≥ 84%, aggregate ≥ 85%, max per-surface regression ≤ 1.0 pp, max aggregate regression ≤ 0.50 pp.

Surface Current Baseline Δ
control-plane 87.80% 87.40% ↑ +0.40 pp 🟡
sdk-go 93.10% 92.00% ↑ +1.10 pp 🟢
sdk-python 94.72% 93.73% ↑ +0.99 pp 🟢
sdk-typescript 91.72% 90.42% ↑ +1.30 pp 🟢
web-ui 84.76% 84.79% ↓ -0.03 pp 🟡
aggregate 85.88% 85.75% ↑ +0.13 pp 🟡

✅ Gate passed

No surface regressed past the allowed threshold and the aggregate stayed above the floor.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📐 Patch coverage gate

Threshold: 80% on lines this PR touches vs origin/main (from .coverage-gate.toml:thresholds.min_patch).

Surface Touched lines Patch coverage Status
control-plane 0 ➖ no changes
sdk-go 0 ➖ no changes
sdk-python 0 ➖ no changes
sdk-typescript 0 ➖ no changes
web-ui 0 ➖ no changes

✅ Patch gate passed

Every surface whose lines were touched by this PR has patch coverage at or above the threshold.

@santoshkumarradha

Copy link
Copy Markdown
Member

The updated implementation and all CI checks look good. The CLA is the only remaining step; once that’s signed, this should be good to go.

@AbirAbbas AbirAbbas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went through this against the real Cursor CLI rather than just reading it: installed the current release (2026.09.08-6caf4ff) and diffed every flag the provider emits against agent --help. All of them are accepted — -p, --trust, --output-format stream-json, --workspace, --mode plan, --force, --model, --resume <id>, prompt last, and CURSOR_API_KEY read from the environment. (Unknown flags and bad --mode values are rejected by the arg parser before the auth check, so a run that gets as far as "Authentication required" proves the rest parsed.) The doctor-spec commit does what it claims too: dropping _availability.py back to main makes four tests in test_harness_doctor.py fail with KeyError: 'cursor', including the generic all-providers one. ruff, the full Python suite (2386 passed) and the coverage/patch gates are green at c85837d.

One thing I think should change before this lands — the default permission_mode mapping. Details inline.

Two smaller notes, take or leave:

  • docs/harness-providers.md still has the install table without a cursor row; a user reading the docs won't find out the binary is called agent.
  • The assistant event in RESULT_STREAM is {"type":"assistant","content":"working"}, but the shipped CLI emits {"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":...}]},"session_id":...}. Nothing breaks today because the final text and the chat id both come off the result event, but the fixture isn't pinning the shape the CLI actually produces.

Comment thread sdk/python/agentfield/harness/providers/cursor.py Outdated
… is unset

An unset permission_mode mapped to --mode plan, which is read-only. The
harness produces schema output by asking the agent to write a file, so
app.harness(prompt, schema=..., provider="cursor") could never succeed,
and plain task runs silently edited nothing. The premise was wrong too:
--mode only accepts plan|ask, the CLI never defaults to ask, and headless
runs auto-reject approval requests instead of hanging.

Unset now emits neither --mode nor --force, matching codex, gemini, pi
and claude-code. Also adds the cursor row to the provider install table
and gives the test stream the assistant event shape the CLI emits.
@dchaudhari7177

Copy link
Copy Markdown
Author

Took both smaller notes as well, in the same commit (01141cb):

  • Docs: docs/harness-providers.md now has a cursor row in the install table (curl https://cursor.com/install -fsS | bash, required CLI agent, auth agent login or CURSOR_API_KEY). The note below the table now says cursor, like grok, is Python-SDK only, and that the binary is agent, not cursor.
  • Fixture: the assistant event in RESULT_STREAM now has the shape the CLI emits ({"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":…}]},"session_id":…}). Final text and chat id still come off the result event, and every test passes against the new shape.

Ruff (the pinned v0.15.22) is clean on both Python files.

Thanks for testing this against the real CLI. The --mode plan default would have broken every schema run.

@santoshkumarradha santoshkumarradha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for correcting the default permission behavior. Leaving the mode unset preserves the Cursor CLI default and allows schema runs to write their output, while the explicit plan and auto mappings remain intact. The focused provider, doctor, factory, defaults, and packaging tests pass locally, and Ruff is clean.

@AbirAbbas AbirAbbas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked this against the installed Cursor CLI (2026.09.08) rather than just the tests: every flag the provider can emit is in agent --help, bare -p is the write+shell mode as the fix assumes, the key only ever travels via CURSOR_API_KEY, and the stream-json fixture matches the CLI's bundled output formatter. Full sdk-python gates pass on the merge with main (ruff 0.15.22, run_pytest.sh 2428 passed, websockets 12/15 compat), and the harness lines the patch gate can't see are 100% covered by the cursor/doctor tests. Code is good to go.

@AbirAbbas

Copy link
Copy Markdown
Contributor

Closing only because the CLA is still unsigned (status has been pending since Sep 6, reminder on Sep 8) and we keep the open queue to things that can merge. The code itself is approved by both of us and needs no further changes. Sign it at https://cla-assistant.io/Agent-Field/agentfield?pullRequest=1042 and reopen this PR (or ping me) and it goes straight into the merge queue.

@AbirAbbas AbirAbbas closed this Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[harness] Add Cursor CLI provider to Python SDK

4 participants