Add Cursor as a first-class provider in T3 Code using ACP (agent acp) over JSON-RPC 2.0 stdio, with robust session lifecycle handling and canonical ProviderRuntimeEvent projection.
- Binary is
agenton PATH (2026.02.27-e7d2ef6observed). - ACP server command is
agent acp. - Transport is newline-delimited JSON-RPC 2.0 over stdio.
- Messages:
- client -> server: requests and responses to server-initiated requests
- server -> client: responses, notifications (
session/update), and server requests (session/request_permission)
initializereturns:protocolVersionagentCapabilities(loadSession,mcpCapabilities,promptCapabilities)authMethods(includescursor_login)
authenticate { methodId: "cursor_login" }returns{}when logged in.session/newreturns:sessionIdmodes(agent,plan,ask)
session/loadworks and requiressessionId,cwd,mcpServers.session/promptreturns terminal response{ stopReason: "end_turn" | "cancelled" }.
Important sequence note:
- ACP currently allows
session/neweven without explicitinitialize/authenticatewhen local auth already exists. - For adapter consistency and forward compatibility, we should still send
initializeandauthenticateduring startup.
Observed params.update.sessionUpdate values:
available_commands_updateagent_thought_chunkagent_message_chunktool_calltool_call_update
Observed payload behavior:
agent_*_chunkprovidescontent: { type: "text", text: string }.tool_callmay be emitted multiple times for sametoolCallId:- initial generic form (
title: "Terminal",rawInput: {}) - enriched form (
title: "\pwd`",rawInput: { command: "pwd" }`)
- initial generic form (
tool_call_updatestatuses observed:in_progresscompleted
tool_call_updateon completion may includerawOutput:- terminal:
{ exitCode, stdout, stderr } - search/find:
{ totalFiles, truncated }
- terminal:
- ACP server sends
session/request_permission(JSON-RPC request withid). - Request shape includes:
params.sessionIdparams.toolCallparams.options(allow-once,allow-always,reject-once)
- Client must respond on same
idwith:{ outcome: { outcome: "selected", optionId: "<one-option-id>" } }
- Reject path still results in tool lifecycle completion events (
tool_call_update status: completed), typically withoutrawOutput.
session/cancelcurrently returns:- JSON-RPC error
-32601Method not found
- JSON-RPC error
- Error shape examples:
- unknown auth method:
-32602 session/loadmissing/invalid params:-32602session/promptunknown session:-32603with details
- unknown auth method:
- Parallel prompts on same session are effectively single-flight:
- second prompt can cause first to complete with
stopReason: "cancelled".
- second prompt can cause first to complete with
session/newaccepts amodelfield (no explicit echo in response).
Probe artifacts:
.tmp/acp-probe/*/transcript.ndjson.tmp/acp-probe/*/summary.jsonscripts/cursor-acp-probe.mjs
- T3 adapter contract still requires:
startSession,sendTurn,interruptTurn,respondToRequest,readThread,rollbackThread,stopSession,listSessions,hasSession,stopAll,streamEvents.
- Orchestration consumes canonical
ProviderRuntimeEventonly. ProviderCommandReactorprovider precedence fix remains required (respect explicit provider on turn start).- ACP now supports external permission decisions, so Cursor can participate in T3 approval UX via adapter-managed request/response plumbing.
apps/server/src/provider/Services/CursorAdapter.ts(service contract/tag + ACP event schemas).apps/server/src/provider/Layers/CursorAdapter.ts(single implementation unit; owns ACP process lifecycle, JSON-RPC routing, runtime projection).- No manager indirection; keep logic in layer implementation.
- One long-lived ACP child process per T3 Cursor provider session.
- Track:
providerSessionId(T3 synthetic ID)acpSessionId(fromsession/newor restored viasession/load)cwd,model, in-flight turn state- pending permission requests by JSON-RPC request id
- Resume support:
- persist
acpSessionIdin provider resume metadata and callsession/loadon reattach.
- persist
startSession:- spawn
agent acp initializeauthenticate(cursor_login)(best-effort, typed failure handling)session/neworsession/load
- spawn
sendTurn:- send
session/prompt { sessionId, prompt: [...] } - consume streaming
session/updatenotifications until terminal prompt response
- send
interruptTurn:- no native
session/canceltoday; implement fallback:- terminate ACP process + restart +
session/loadfor subsequent turns - mark in-flight turn as interrupted/failed in canonical events
- terminate ACP process + restart +
- no native
respondToRequest:- map T3 approval decision -> ACP
optionId - reply to exact JSON-RPC request id from
session/request_permission
- map T3 approval decision -> ACP
- Keep logic inside
CursorAdapterLive. - Use Effect primitives:
Queue+Stream.fromQueuefor event fan-outRef/Ref.Synchronizedfor session/process/request state- scoped fibers for stdout/stderr read loops
- Typed JSON decode at boundary:
- request/response envelopes
session/updateunion schema- permission-request schema
- Keep adapter errors in typed error algebra with explicit mapping at process/protocol boundaries.
session/update: agent_message_chunk- emit
message.deltafor assistant stream
- emit
- prompt terminal response (
session/promptresultstopReason: end_turn)- emit
message.completed+turn.completed
- emit
session/update: agent_thought_chunk- initial mapping: emit thinking activity (or ignore if we keep current canonical surface minimal)
session/update: tool_call- first-seen
toolCallIdemitstool.started - subsequent
tool_callfor same ID treated as metadata update (no duplicate started event)
- first-seen
session/update: tool_call_updatein_progress: optional progress activitycompleted: emittool.completedwith summarizedrawOutputwhen present
session/request_permission- emit
approval.requestedwith mapped options - when client decision sent, emit
approval.resolved
- emit
- protocol/process error
- emit
runtime.error - fail active turn/session as appropriate
- emit
Synthetic IDs:
turnId: T3-generated UUID persendTurn.itemId:- assistant stream:
${turnId}:assistant - tools:
${turnId}:${toolCallId}
- assistant stream:
- Cursor ACP permission requests are externally controllable; implement full
respondToRequestpath in v1. - Decision mapping:
- allow once ->
allow-once - allow always ->
allow-always - reject ->
reject-once
- allow once ->
session/loadis available and should be first-class for adapter restart/reconnect.- Must send required params:
sessionId,cwd,mcpServers.
- ACP currently has no observed rollback API.
- Plan for v1:
readThread: adapter-maintained snapshot projectionrollbackThread: explicit unsupported error
- Product guard:
- disable checkpoint revert for Cursor threads in UI until rollback exists.
- Add
cursortoProviderKind. - Add Cursor provider start options (
providerOptions.cursor), ACP-oriented:- optional
binaryPath - optional auth/mode knobs if needed later
- optional
- Extend model options for Cursor list and traits mapping.
- Add schemas for ACP-native event union in Cursor adapter service file.
- Register
CursorAdapterin provider registry and server layer wiring. - Update provider-kind persistence decoding for
cursor. - Fix
ProviderCommandReactorprecedence to honor explicit provider in turn-start command.
- Cursor in provider picker and model picker (already partially done).
- Trait controls map to concrete Cursor model identifiers.
- Surface unsupported rollback behavior in UX.
- Implement ACP process lifecycle in
CursorAdapterLive. - Implement JSON-RPC request/response multiplexer.
- Implement
initialize/authenticate/session/new|loadflow. - Wire
streamEventsfrom ACP notifications.
- Map
session/updatevariants to canonical runtime events. - Implement permission-request bridging to
respondToRequest. - Implement dedupe for repeated
tool_callon sametoolCallId.
- Implement single in-flight prompt protection per session.
- Implement interruption fallback (process restart + reload) because
session/cancelunavailable. - Ensure clean state recovery on ACP process crash.
- Provider routing precedence fix.
- Cursor-specific UX notes for unsupported rollback.
- End-to-end smoke and event log validation.
Follow project rule: backend external-service integrations tested via layered fakes, not by mocking core business logic.
- JSON-RPC envelope parsing:
- response matching by id
- server request handling (
session/request_permission) - notification decode (
session/update)
- Event projection:
agent_message_chunk/agent_thought_chunktool_call+tool_call_updatededupe/lifecycle- permission request -> approval events
- Error mapping:
- unknown session
- method-not-found (
session/cancel) - invalid params
- Registry resolves
cursor. - Session directory persistence reads/writes
cursor. - ProviderService fan-out ordering with Cursor ACP events.
thread.turn.startwithprovider: cursorroutes to Cursor adapter.- approval response command maps to ACP permission response.
- checkpoint revert on Cursor thread returns controlled unsupported failure.
- Env-gated ACP smoke:
- start session
- run prompt
- observe deltas + completion
- exercise permission request path with one tool call
- Keep one in-flight turn per ACP session.
- Keep per-session ACP process logs/NDJSON artifacts for debugging.
- Treat
session/cancelas unsupported until Cursor ships it; avoid relying on it. - Preserve resume metadata (
acpSessionId) for crash recovery.
- Should we call
authenticatealways, or only after auth-required errors? - Should model selection be passed at
session/newonly, or can/should we support model switching mid-session if ACP adds API? - For interruption UX, do we expose “hard interrupt” semantics (process restart) explicitly?
- Plan/documentation switched from headless
agent -pto ACPagent acp. - Contracts updated (
ProviderKind, Cursor options, model/trait mapping). - Cursor ACP adapter layer implemented and registered.
- Provider precedence fixed in orchestration router.
- Approval response path wired through ACP permission requests.
- Tests added for protocol decode, projection, approval flow, and routing.
- Lint + tests green.