Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/acp.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,11 @@ failure fails that room turn visibly instead of falling back.
`OMP_MODEL` env variable. Set `api_key` with it and the adapter passes the key in the env
variable that model's provider needs.
- **Cursor:** question, plan and permission decisions default to `manual`, resolved by a
room participant with `/cursor <word> <token>`. Cursor omits the session id on its
room participant with `/cursor <word> <token>`. Band's own tools, including
`additional_tools`, are approved once per call in every `approval_mode` during
an active turn. If Cursor offers no allow-once option, the configured approval
policy applies instead. Late Band permission requests are refused after the
turn closes or is interrupted. Cursor omits the session id on its
extension notifications, so the adapter holds a turn lock and binds them to that turn's
session; Cursor turns are serialized. Decision prompts, timeout notices and `/cursor`
replies post through `send_notice`, so they never count as the model's reply, and a
Expand Down
35 changes: 26 additions & 9 deletions src/band/adapters/cursor_acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,7 @@
CursorQuestion,
parse_cursor_questions,
)
from band.integrations.acp.client_runtime import (
ALLOW_ALWAYS_KIND,
ACPRuntime,
option_id_of_kind,
permission_option_ids,
select_allow_option_id,
)
from band.integrations.acp.client_runtime import ACPRuntime
from band.integrations.acp.client_types import ACPClientSessionState
from band.integrations.acp.cursor import (
CURSOR_CLI_BINARY,
Expand All @@ -45,6 +39,14 @@
PLAN_REQUESTED_TEMPLATE,
ROOM_COMMAND,
CursorCommandWord,
canonicalize_cursor_tool_name,
)
from band.integrations.acp.permissions import (
ALLOW_ALWAYS_KIND,
ALLOW_ONCE_KIND,
option_id_of_kind,
permission_option_ids,
select_allow_option_id,
)
from band.integrations.acp.session_config import SessionConfigResolver
from band.runtime.custom_tools import CustomToolDef
Expand Down Expand Up @@ -79,8 +81,10 @@ class CursorACPAdapterConfig(ACPClientAdapterConfig):
api_key: Sets ``CURSOR_API_KEY`` unless ``env`` already does;
exclusive with ``auth_token``.
auth_token: Sets ``CURSOR_AUTH_TOKEN`` unless ``env`` already does.
approval_mode: How Cursor's permission requests are decided;
``"manual"`` asks the room.
approval_mode: How Cursor's own tools are decided; ``"manual"`` asks
the room. Band tools, including additional_tools, are approved
once per call in every mode while the turn is active. If Cursor
offers no allow-once option, the configured policy applies.
question_mode: How Cursor's questions are answered; ``"manual"`` asks
the room.
plan_mode: How Cursor's plans are settled; ``"manual"`` asks the room.
Expand Down Expand Up @@ -188,6 +192,9 @@ def _credential_env(self) -> dict[str, str]:
}
return {name: value for name, value in credentials.items() if value}

def _canonical_tool_name(self, name: str) -> str:
return canonicalize_cursor_tool_name(name, self._own_tool_names)

async def on_message(
self,
msg: PlatformMessage,
Expand Down Expand Up @@ -318,6 +325,9 @@ async def on_cleanup(
# Wakes any decision _run_turn is parked on; the task itself keeps
# running detached and winds down on its own (via _on_background_task_done)
# once the runtime this stops out from under it closes the connection.
turn = self._active_turn
if turn is not None and turn.room_id == room_id:
turn.session_id = None
self._cancel_room_decisions(room_id)
await super().on_cleanup(room_id, expected_runtime=expected_runtime)

Expand All @@ -334,6 +344,7 @@ async def on_interrupt(self, room_id: str, mode: ControlMode) -> None:
missing reply."""
turn = self._active_turn
if turn is not None and turn.room_id == room_id:
turn.session_id = None
turn.tools.turn.settle()
self._cancel_room_decisions(room_id)

Expand All @@ -345,6 +356,12 @@ async def cleanup_all(self, *, final: bool = True) -> None:
async def _resolve_cursor_permission(
self, request: ACPPermissionRequest
) -> str | None:
if request.tool_call.name in self._own_tool_names:
if self._active_turn_for(request.room_id, request.session_id) is None:
return None
once = option_id_of_kind(request.options, ALLOW_ONCE_KIND)
if once is not None:
return once
match self.config.approval_mode:
case "auto_accept":
return select_allow_option_id(request.options)
Expand Down
4 changes: 2 additions & 2 deletions src/band/adapters/omp_acp.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
PermissionResolver,
SpawnProcess,
)
from band.integrations.acp.client_runtime import (
ACPCollectingClient,
from band.integrations.acp.collecting import ACPCollectingClient
from band.integrations.acp.permissions import (
ElicitationHandler,
ElicitationNarrator,
elicitation_requested_schema,
Expand Down
26 changes: 13 additions & 13 deletions src/band/integrations/acp/client_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,27 @@
PlatformMessage,
)
from band.integrations.acp.client_profiles import ACPClientProfile
from band.integrations.acp.client_runtime import (
ACPCollectingClient,
ACPConnectionProtocol,
ACPRuntime,
ElicitationHandler,
ElicitationNarrator,
PermissionHandler,
PermissionNarrator,
allow_permission,
cancel_permission,
permission_option_ids,
select_allow_option_id,
)
from band.integrations.acp.client_runtime import ACPRuntime
from band.integrations.acp.client_types import (
ACPClientSessionState,
BandACPClient,
)
from band.integrations.acp.collecting import ACPCollectingClient
from band.integrations.acp.model_selection import (
ACPModelOptions,
apply_model_selection,
locate_model_options,
)
from band.integrations.acp.permissions import (
ElicitationHandler,
ElicitationNarrator,
PermissionHandler,
PermissionNarrator,
allow_permission,
cancel_permission,
permission_option_ids,
select_allow_option_id,
)
from band.integrations.acp.room_emitter import RoomTurnEmitter
from band.integrations.acp.session_config import (
CONFIG_FAILURE_PREFIX,
Expand All @@ -91,6 +90,7 @@
SessionConfigSetter,
apply_session_config_selections,
)
from band.integrations.acp.transport import ACPConnectionProtocol
from band.integrations.acp.types import ACPToolCall
from band.integrations.mcp import (
BandMCPBackend,
Expand Down
Loading
Loading