Skip to content

Adaptive reasoning and direct Cursor mode - #27

Closed
nvdorman wants to merge 42 commits into
enowdev:mainfrom
nvdorman:feature/cursor-agent-provider-impl
Closed

Adaptive reasoning and direct Cursor mode#27
nvdorman wants to merge 42 commits into
enowdev:mainfrom
nvdorman:feature/cursor-agent-provider-impl

Conversation

@nvdorman

Copy link
Copy Markdown
Contributor

Follow-up to #25, which merged the Cursor Cloud Agent integration. This adds
the two things that integration was missing: reasoning controls that match the
model actually selected, and a way to run a Cursor agent from the composer
without asking an Antares model to manufacture a tool call.

origin/main is merged in (22392df) rather than rebased, so main's
fix(config): ship the Cursor provider disabled by default and its
ReasoningBlock freeze fix are preserved rather than reverted by this branch's
older base.

Summary

Model-aware reasoning. The global none|low|medium|high enum is replaced by
a capability resolved from the selected provider and model. Auto is the new
default and sends no reasoning field at all. Values stay opaque provider
strings — extra-high is never rewritten to xhigh. Off is offered only where
reasoning can genuinely be disabled. Adapter request bodies were corrected so
every displayed choice has its advertised effect: Anthropic adaptive thinking
with output_config.effort, Gemini thinkingLevel (where minimal means
Minimal, not Off), OpenAI/Codex nested vs flat effort, and OpenRouter sending an
explicit disable instead of omitting the field. Preferences are stored per
provider/model, so a value cannot leak across a model switch.

Direct Cursor mode. Cursor models appear in composer search as a separate
execution target; selecting one never changes model.provider or
model.default. POST /api/chat/cursor runs the turn through a shared
internal/cursorrun service and streams over the existing SSE envelope. The
live catalogue is authoritative: Antares copies one concrete variant's whole
param array (including params Cursor does not publish as user-facing, such as
cyber) and never synthesises a combination Cursor did not return.

Safety boundaries. Start, follow-up, and cancel each require an explicit
approval even under approval_mode: auto, and the retained request — not the
displayed card — is what executes. Create requests are never auto-retried; an
ambiguous failure is recorded as ambiguous instead. Local Stop only detaches and
can only win before the non-idempotent POST; remote Cancel is a separate
approved action. Runs survive browser detach and daemon restart via persisted
agent/run IDs, with reset-and-replay rather than duplicated text on resume.
API keys are redacted from every error, event, log, and persisted field.

Test plan

  • go test ./internal/llm ./internal/agent ./internal/config ./internal/cursor ./internal/cursorrun ./internal/approval ./internal/tools ./internal/store ./internal/server ./cmd/antares -count=1
  • go test -race over the same packages — clean
  • make check (vet, full go test ./..., typecheck, 154 web tests) — green
  • make smoke — all 16 dashboard routes clean
  • Live Cursor verification was metadata-only: GET /v1/me and
    GET /v1/models (34 models, all with variants; confirmed the hidden
    cyber variant param exists upstream). No agent, run, follow-up, or
    cancellation was created.
  • Approval boundary covered by regression tests rather than a paid call:
    no upstream request before approval, deny sends nothing, approval payload
    bounded/redacted/immutable, and local interrupt does not cancel remotely.

Suites were run with credential variables stripped so nothing silently went live.

Known issue (pre-existing, not from this branch)

internal/mcp is flaky under full-suite parallel load — TestStdioRoundTrip
and TestRefreshReplacesToolsAndReadiness can exceed their 30s subprocess
deadline. Reproduced on unmodified origin/main (1/9 full-suite runs) as well
as here, in a package this branch does not touch. Left alone rather than
folded into this PR.

Made with Cursor

nvdorman and others added 30 commits August 13, 2026 00:27
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Three blocking issues from review, fixed without weakening validation
for newly-explicit invalid values:

1. Default OpenRouter/direct-Anthropic chat failed pre-request. The
   config default carried a non-empty "medium" reasoning effort with no
   attached capability, and openai-compatible's static resolution is
   deliberately nil, so every default chat errored before the network.
   Direct Anthropic also failed because the runtime default base URL
   ("https://api.anthropic.com/v1") didn't match the static catalog's
   bare-host check. Fixed by defaulting reasoning effort to "" (Auto,
   valid everywhere) and widening the catalog's Anthropic base URL
   match to an exact two-value allowlist.
2. Bedrock bypassed reasoning validation by calling anthropic buildBody
   directly, silently dropping invalid explicit values and sending
   signed requests upstream. bedrockClient.Chat now runs the same
   validateReasoning gate before signing/sending.
3. A legacy Anthropic model could pass attached-capability validation
   for a value absent from the hardcoded budget table, silently
   vanishing from the request. Factored the body-shape decision into
   anthropicThinkingBody, shared by buildBody and a new
   validateReasoning check so an unmappable value now fails before
   any request.

Co-authored-by: Cursor <cursoragent@cursor.com>
1. Direct Gemini's static catalogue required provider id "google", but
   the shipped provider default (internal/config/defaults.go) uses
   "gemini" as its provider map key, so the static fallback never fired
   for the real default Gemini provider. Widened the match to an exact
   two-value allowlist (isGeminiDirectProvider): "google" or "gemini",
   still rejecting arbitrary compatible/custom provider ids.

2. geminiThinkingConfig returned nil for both "no override" and "cannot
   map this effort", so a value that passed an attached
   ReasoningCapability but had no real mapping (unrecognised keyword, or
   "minimal" on a legacy budget model) silently vanished from the
   request instead of failing. geminiThinkingConfig now returns an
   explicit ok bool distinguishing the two; a new
   geminiClient.validateReasoning fails Chat/Stream before any network
   call when ok is false, while buildBody stays a pure, non-erroring
   body builder. Known low/medium/high/minimal/none mappings for both
   Gemini 3 (thinkingLevel) and 2.5 (thinkingBudget) models are
   unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Reuse scoped catalogues and preserve stale capabilities so interactive reasoning remains deterministic across repeated lookups and transient provider failures.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep catalogue fetches independent of individual waiters and retain resolved adapter identity without widening explicit Codex capabilities.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Validate raw edits against their candidate provider map without touching live config, and preserve repairability while distinguishing unavailable metadata from unsupported values.

Co-authored-by: Cursor <cursoragent@cursor.com>
Apply provider credential and endpoint overrides to write-free candidate parsing so capability checks match the eventual reload without persisting derived values.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep opaque preferences intact while model metadata is unavailable and resolve role/config capabilities through deduplicated targeted lookups.

Co-authored-by: Cursor <cursoragent@cursor.com>
Collapse free-text target edits to one stale-safe request and bound authoritative metadata caching to prevent lookup storms and partial-key growth.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Ensure every explicit operation is safely announced through one approval policy path and fully redact key-like display fields.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Preserve recoverability across local detach and restart while making ambiguous outcomes explicitly reconcilable without duplicate remote mutations.

Co-authored-by: Cursor <cursoragent@cursor.com>
Ensure every recovery follower replays durable state and prevent automated cleanup from deleting active remote Cursor sessions across selection races.

Co-authored-by: Cursor <cursoragent@cursor.com>
nvdorman and others added 12 commits August 13, 2026 12:04
Keep compacted SSE replay reconstructable and protect terminal work while making bulk deletion and cancellation classification deterministic.

Co-authored-by: Cursor <cursoragent@cursor.com>
Treat local create configuration failures definitively, make exact-set deletions atomic, and explain compacted tool activity without retaining payloads.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Leaving conversation B for A and returning gives two openings that share
every textual identifier, and a navigation commits before its passive
cleanup runs. The pending-approval refresh and the standing attachment
were scoped by session id plus an `alive` flag, so a response, frame,
error, retry, end-of-turn refresh, or finally callback from the first
opening could still write into the second.

Both operations now carry the exact route-open occurrence and ask it
again at the instant they would write.

Co-authored-by: Cursor <cursoragent@cursor.com>
…provider-impl

Main advanced 15 commits past the PR enowdev#25 merge that this branch forked
from, including the fix that ships the Cursor provider disabled by
default. Merging before the follow-up PR keeps that fix rather than
letting a stale `Enabled: true` ride back in, and pins the branch's new
Cursor suites against the default a user will actually get.

Three files overlapped and auto-merged cleanly:
- internal/config/defaults.go: main's cursor `Enabled: false` alongside
  this branch's Auto (`""`) reasoning defaults.
- internal/tools/cursor_agent_test.go: main's explicit `p.Enabled = true`
  fixture.
- web/src/pages/ChatPage.tsx: main's ReasoningBlock expand-freeze fix.

Co-authored-by: Cursor <cursoragent@cursor.com>
The reasoning docs still described the old global none|low|medium|high
ladder and a `medium` default, and the Cursor docs still described the
tool-only integration that shipped enabled. Both now match what the code
does.

Configuration gains a Reasoning section: Auto sends no field at all,
values are opaque provider strings that are never normalised across
providers, Off appears only where reasoning can actually be disabled, and
the per-provider table records how Anthropic, Gemini, OpenAI/Codex and
OpenRouter each spell it. The Cursor entry is documented as disabled by
default, with the repository/ref rules and the warning that a cloud VM
cannot see dirty files or local-only commits.

Tools documents direct Cursor mode: exact variant selection with no
synthesised combinations, the approval card and its immutable retained
request, local Stop versus approval-gated remote Cancel, what keeps a
follow-up on the same agent, and the five-image/15 MiB attachment limit.

Verification states the metadata-only rule outright, adds a deny-at-the-
approval-card dashboard pass that costs nothing, and records how to strip
credential variables for a hermetic suite.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two documentation claims were wrong in a way that matters for trust.

"No request reaches Cursor before you approve" was false: the composer
cannot draw the model list, the variant options, or the approval card
without reading /v1/me and /v1/models first. The verification pass then
contradicted itself by asking the reader to confirm the live catalogue
loads in steps 2-3 and calling the whole pass request-free in step 5.
Both now state the guarantee that actually holds — no create-agent,
create-run, or cancel is sent before its own approval — and say plainly
that read-only metadata GETs do happen earlier and cost nothing.

The design said a model with no variants uses an empty parameter list,
which contradicts the exact-authoritative-variant rule in the same
section and the implementation: defaultCursorVariant returns null,
cursorModelSelectable is false, cursorTargetFromModel yields no target,
and the picker renders the row disabled. `{"params": []}` would be a
selection Cursor never offered. The design now matches, and separates
the one legitimate no-parameter case — the cursor_agent tool omitting
model_params so Cursor applies its own default, which is an absent field
rather than an empty array and is not reachable from the composer.

Co-authored-by: Cursor <cursoragent@cursor.com>
@nvdorman

Copy link
Copy Markdown
Contributor Author

Ditutup atas permintaan pemilik repo. Pendekatan Cursor Cloud Agent dinilai tidak cocok untuk alur kerja lokal; arah integrasi akan dipertimbangkan ulang. Commit tetap tersimpan di branch feature/cursor-agent-provider-impl.

@nvdorman nvdorman closed this Aug 13, 2026
@nvdorman
nvdorman deleted the feature/cursor-agent-provider-impl branch August 13, 2026 10:40
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.

1 participant