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: 3 additions & 3 deletions .github/scripts/setup-omp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# Install Bun and pin OMP (@oh-my-pi/pi-coding-agent) for the `backends` e2e lane.
set -euo pipefail

OMP_PACKAGE="${OMP_PACKAGE:-@oh-my-pi/pi-coding-agent@18.2.8}"
OMP_MIN_BUN="${OMP_MIN_BUN:-1.3.14}"
OMP_PACKAGE="${OMP_PACKAGE:-$(uv run python -c 'from band.integrations.omp import OMP_PINNED_PACKAGE; print(OMP_PINNED_PACKAGE)')}"
OMP_MIN_BUN="${OMP_MIN_BUN:-$(uv run python -c 'from band.integrations.omp import OMP_MIN_BUN; print(OMP_MIN_BUN)')}"

if ! command -v bun >/dev/null 2>&1; then
curl -fsSL https://bun.sh/install | bash
Expand Down Expand Up @@ -31,6 +31,6 @@ fi
bun --version
bun install -g "${OMP_PACKAGE}"
omp --version
omp acp --help >/dev/null
omp --mode rpc --help >/dev/null

echo "OMP ready (package=${OMP_PACKAGE}, min Bun=${OMP_MIN_BUN})"
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ jobs:
path: ~/.cursor-agent-*
key: ${{ steps.cursor-cli-cache.outputs.cache-primary-key }}

- name: Install Bun and OMP (omp_acp)
- name: Install Bun and OMP (omp)
if: matrix.lane == 'backends'
run: bash .github/scripts/setup-omp.sh

Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/python-core-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ concurrency:

jobs:
coverage:
# Dependabot cannot read repository Actions secrets, so it cannot check
# out the private Core repository with this workflow's token.
if: github.actor != 'dependabot[bot]'
# Fork pull requests and Dependabot cannot read repository Actions secrets.
# Validate fork revisions through a reviewed upstream branch and manual dispatch.
if: >-
github.actor != 'dependabot[bot]' &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,8 @@ LangGraph supports the built-in Band platform tools, custom LangChain tools thro
| ------------ | ------------- | ------------------------------------ | --------------------------------------------- |
| A2A bridge | `a2a` | `A2AAdapter` | [examples](examples/a2a_bridge/) |
| A2A gateway | `a2a_gateway` | `A2AGatewayAdapter` | [examples](examples/a2a_gateway/) |
| ACP | `acp` | `ACPClientAdapter`, `CopilotACPAdapter`, `OmpACPAdapter`, `ACPServer`, `BandACPServerAdapter` | [examples](examples/acp/) |
| ACP | `acp` | `ACPClientAdapter`, `CopilotACPAdapter`, `ACPServer`, `BandACPServerAdapter` | [examples](examples/acp/) |
| OMP native RPC | `omp` | `OmpAdapter` | [example](examples/omp.py), [setup](docs/acp.md#omp-native-rpc) |
| Slack | `slack` | `SlackAdapter` | [examples](examples/slack/) |

> **Other languages:** The Band SDK is also available for [TypeScript](https://github.com/thenvoi/thenvoi-sdk-typescript).
Expand All @@ -497,7 +498,7 @@ Additional bridge extras exist for specialized deployments: `a2a_gateway_demo` s

Agents using the Band SDK can receive built-in tools for interacting with Band. **Chat tools are always enabled**, and cannot be disabled. Contact and memory tools are opt-in capabilities, configured via `capabilities=` on the adapters that support them, and are disabled unless you explicitly enable them.

The table below is the agent tool surface exposed to LLM adapters. Framework adapters in [Supported Adapters](#supported-adapters) support `Capability.CONTACTS` and `Capability.MEMORY`; the A2A adapters (`A2AAdapter`, `A2AGatewayAdapter`) and the ACP server adapter (`BandACPServerAdapter`) declare no capabilities, so `capabilities=` has nothing to enable there. `ACPClientAdapter` and the Copilot, Cursor and OMP backends built on it accept them.
The table below is the agent tool surface exposed to LLM adapters. Framework adapters in [Supported Adapters](#supported-adapters) support `Capability.CONTACTS` and `Capability.MEMORY`; the A2A adapters (`A2AAdapter`, `A2AGatewayAdapter`) and the ACP server adapter (`BandACPServerAdapter`) declare no capabilities, so `capabilities=` has nothing to enable there. `ACPClientAdapter`, its Copilot and Cursor backends, and the native `OmpAdapter` accept them.

| Category | Tool Names | What They Enable |
| ------------ | ---------- | ---------------- |
Expand Down
181 changes: 164 additions & 17 deletions docs/acp.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# ACP (Agent Client Protocol) Integration
# ACP and native OMP integrations

Facts about the ACP integration that span modules or that the code cannot say for
itself. The client side is `ACPClientAdapter` (Band room → room-owned ACP subprocess);
the server side is `ACPServer` + `BandACPServerAdapter`.
Facts about the ACP and native OMP integrations that span modules or that the
code cannot say for itself. The ACP client side is `ACPClientAdapter` (Band room →
room-owned ACP subprocess); the server side is `ACPServer` +
`BandACPServerAdapter`. OMP uses a separate [native RPC integration](#omp-native-rpc),
not ACP.

Client-side automatic idle release is unavailable pending provider-specific
safety qualification; see [idle room resource retention](idle-room-release.md).

## Configuration

`ACPClientAdapterConfig` holds the plain settings; `CursorACPAdapterConfig`,
`CopilotACPAdapterConfig` and `OmpACPAdapterConfig` extend it with backend defaults
and fields. Callables (`workspace_for_room`, `resolve_session_config`,
`resolve_permission`) are keyword-only constructor arguments.
`ACPClientAdapterConfig` holds the plain settings; `CursorACPAdapterConfig` and
`CopilotACPAdapterConfig` extend it with backend defaults and fields. Callables
(`workspace_for_room`, `resolve_session_config`, `resolve_permission`) are
keyword-only constructor arguments.

```python
from band.adapters import ACPClientAdapter, ACPClientAdapterConfig
Expand Down Expand Up @@ -169,10 +171,10 @@ assert slow.retention.transition_timeout_s == 120
overdue server close stays owned and a retry joins it. A stop is final: later
turns are refused until `on_started` (an `Agent.start()`) finishes it and serves
again.
- **Idle release stays off.** OMP, Cursor and Copilot report `ProviderActivity.UNKNOWN`,
so an idle release always answers BUSY. Native qualification is still owed: OMP
background jobs (including suppressed or acknowledged work), Cursor work beyond
subagent drain, and Copilot scheduled `/after` prompts. Band MCP calls and ACP
- **Idle release stays off.** Cursor and Copilot report `ProviderActivity.UNKNOWN`,
so an idle release always answers BUSY. Native qualification is still owed for
Cursor work beyond subagent drain and Copilot scheduled `/after` prompts.
Band MCP calls and ACP
permission or extension requests carry no prompt id, so a call arriving after its
turn is served with the room's current tools.

Expand All @@ -185,11 +187,6 @@ failure fails that room turn visibly instead of falling back.

## Backends

- **OMP:** `approval_mode="yolo"` bypasses OMP's native approval and gives the agent full
access to its host. It does not change Band tool registration or platform permissions.
`OmpACPAdapterConfig.model` is passed as OMP's `--model` flag; OMP does not read an
`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>`. Band's own tools, including
`additional_tools`, are approved once per call in every `approval_mode` during
Expand Down Expand Up @@ -217,6 +214,156 @@ failure fails that room turn visibly instead of falling back.
first prompt. The live `backends` lane pins the Cursor CLI and passes `E2E_CURSOR_API_KEY`
as `CURSOR_API_KEY` only to its baseline step; local runs may use a stored `agent login`.

## OMP native RPC

`OmpAdapter`, `OmpAdapterConfig` and `OmpPermissionRequest` live in
`band.adapters.omp` and are exported from `band.adapters`. Install `band-sdk[omp]`
and an OMP executable at version 18.8.9 or later; the executable fixture is pinned
to 18.8.9 and requires Bun 1.3.14 or later. The `omp` extra does not require the ACP
runtime. Native sessions negotiate RPC protocol v2 to preserve structured
text/image tool results.

- **Configuration.** Pass `OmpAdapterConfig` to the adapter. `model` selects
`provider/model`, and `reasoning_effort` selects the native thinking level,
including `"auto"`. Automatic effort stays automatic rather than being replaced
with the effective level chosen for one turn.
`api_key` requires `model` and reaches the child in that provider's environment
variable; OMP does not read an `OMP_MODEL` environment variable.
`workspace_for_room` and `resolve_permission` remain constructor callables.
OMP does not use the ACP `resolve_session_config` callback.
- **Tools and replies.** Canonical Band tools and `additional_tools` are
registered through native `set_host_tools`; results retain text/image content.
Room replies still require `band_send_message`, and deliberate declines use
`band_no_reply`. Native assistant text is optional thought telemetry, not a
room reply. `SimpleAdapter.run_judged_turn` remains the sole turn-judging owner:
native-only output and failed tools followed by narration still reach the
missing-reply verdict.
- **Instructions and attribution.** A private always-apply native rule places Band's
stable communication and completion guidance in OMP's genuine system prompt,
preserving native instructions, user prompt additions and append-file discovery.
A startup command verifies the rule is present before inference, even without
external MCP servers. Disabling rules or using a system-prompt template that
omits them fails startup rather than silently dropping Band's connection policy.
The rule survives restored sessions and prompt rebuilds during background work.
Per-message platform context contains
room, requester, roster and contact facts as JSON, not `[System]`-styled commands.
Replayed history is identified as previously handled data; it is not a new
participant handoff. Native lifecycle fields describe runtime progress, while the
Band turn ledger separately determines whether a reply was delivered or declined.
- **Background settlement.** Prompt acceptance, an initial `prompt_result`, and
`agent_end` with async work remaining are not completion. The reader stays
attached through automatic background continuations and waits for both the
matching prompt outcome and actual native session settlement
(`session_settled` or `get_state.isSettled`). An error in a resumed assistant
message fails the same Band turn. This is a native lifecycle barrier, not an
instruction-only mitigation, and it does not synthesize a reply or decline.
`turn_timeout_s` covers the whole lifecycle. Timeout, errors and cancellation
close that room's process and terminate its captured descendants before returning;
a subsequent message starts a fresh process rather than inheriting unfinished work.
- **Process close.** A usable reader gets a best-effort abort request bounded to
one second, then stdin closes for EOF while output is drained. The root gets
one second to exit before termination and, if needed, kill, each followed by
another one-second exit wait. Captured descendants are terminated and, if
needed, killed concurrently with the root's EOF grace; Python running behind
a Windows virtualenv launcher is one such descendant.
On POSIX, termination sends `SIGTERM` and kill sends `SIGKILL`. On Windows,
both are forced `TerminateProcess` operations, not graceful signal delivery.
Successful close proves the root was waited for and captured descendants
settled; it does not guarantee exit status zero. Caller cancellation still
waits for shared cleanup, and cleanup failures propagate.
- **Isolation and state.** Each room owns a native process, persistent session
and workspace. OMP receives the workspace via `--cwd`; the subprocess itself
starts without a supplied cwd. Session state carries `omp_session_id`,
`omp_session_file` and `omp_room_id`. A workspace is not an OS sandbox.
- **External MCP servers.** `mcp_servers` is a name-to-`OmpMcpServer` map; import
that typed model from `band.integrations.omp`. Stdio entries use `command`,
`args`, `env` and optional `cwd`; remote entries use `url`, `headers` and optional
`type` (`http` or `sse`). Relative stdio `cwd` resolves from the room workspace;
relative path-like commands resolve from that server cwd, or the room workspace
when cwd is omitted, before plugin projection. The adapter writes an owner-only temporary
`band-sdk/.mcp.json` plugin overlay and passes it with `--plugin-dir`. The overlay
stays alive until the child closes, then is removed. User profiles and native
credentials are not rewritten.
The SDK requires native connection and tool-mount receipts for every configured
server before inference. It uses the RPC UI event mode and a private startup
setting to enable those receipts without rewriting user settings. Missing or
failed receipts fail startup rather than allowing a model to answer without its
required tools. Native connection and tool failures remain errors, not successful
Band replies. The enclosing turn timeout still bounds startup.
- **Per-connection MCP servers.** Pass the constructor callable
`resolve_mcp_servers`, an async `(workspace: str) -> Mapping[str, OmpMcpServer]`,
when external servers (or their credentials) can change while the adapter runs.
It is called once for each new room process, with the room's claimed workspace,
before the overlay or process exists; the same result feeds both the overlay and
the startup receipt check. Its result is the room's complete **external** server
set: it replaces every external entry in `mcp_servers` (an empty mapping means
none), and an exception fails that startup with no fallback to them. A `band`
entry in `mcp_servers` is the SDK's own Band endpoint, not an external server: it
is kept beside the resolved set, so Band tools still route through it with
`inject_band_tools=False`. The resolver itself may not return `band`, because OMP
would mint that server's tools under Band tool names. A reused room process never
calls it again; a retired room (cancellation, failure, or cleanup) resolves afresh
on its next start. The callable is runtime-only and never part of
`OmpAdapterConfig`.
- **Room retirement.** `on_cleanup` starts one retirement per room, and overlapping
`on_cleanup` and `cleanup_all` calls join it. The room's active turn is the
current `on_message` call, not the task that made it. A cancellation already
requested when that call starts is delivered at its first step, before anything
is resolved or launched; one its caller had already received and handled does
not stop it. Retirement marks the active turn retired, so nothing it resolves
afterwards can start a process. While the turn is still running, retirement
becomes the cause of its end: it disarms the turn deadline if that has not fired,
so the deadline cannot cancel the turn's settlement again, and cancels the turn
once, or joins a cancellation it is already handling. A turn whose deadline has
fired, or that is already handling its own outcome, keeps its timeout or other
result and its failure report. A fired deadline counts as that cancellation even
if the resolver consumed it with `uncancel()`: cleanup joins it rather than
cancelling again, and a server set the resolver returns afterwards is refused, so
the turn ends with `TimeoutError` and nothing is launched. A turn that has already
finished is never
cancelled, so its caller's later work is untouched. Retirement then waits for
that call's own finalization (not for the rest of its caller's work), and only
then closes the process and releases the workspace. A turn may request cleanup of
its own room, for example from its resolver: that call returns at once, the turn
is not cancelled again but its unfired deadline is disarmed all the same, it stops
before launching, and teardown follows its settlement. Cancelling a cleanup
call stops only that caller's wait. Until the retirement ends, turns already
waiting for the room and turns that arrive during it are cancelled rather than
starting a new process. A later turn starts normally. A room whose native close
fails keeps its process handle, overlay and exclusive workspace claim until a
later close succeeds: `on_cleanup` raises the failure, `cleanup_all` and the
room's next turn retry the close first, and the retiring process never receives
new work.
- **Permissions.** `approval_mode="yolo"` bypasses native approval checks without
changing Band platform permissions. Otherwise `resolve_permission` receives
`OmpPermissionRequest(room_id, session_id, title, options)` and returns an
offered option or `None`. Unknown or unoffered choices cancel the request.
Without a resolver, only the exact native `("Approve", "Deny")` select is
approved. Unsupported interactive forms are cancelled; this is not approval
for arbitrary UI requests.
A cancelled select posts an independent `omp_permission` call/result pair only
with `Emit.TOOL_CALLS`. Offered selections stay silent; option labels do not
determine an SDK denial outcome. Permission narration records no turn effect
or chat reply. Permission titles and native events share full-registry privacy
redaction, including `mcp__` names and executed `xd://` device writes. Bare Band
aliases use the canonical privacy policy without relabelling native events.
Native names and turn effects still use enabled capabilities plus contacts;
withheld device writes retain their path, other fields and string payload.
Unparseable arguments, including decoder integer-limit errors, use a fixed
placeholder. Synthetic IDs use `band-permission:`; metadata retains the
original `omp_request_id`. Native event IDs are never changed or merged with
permission records.
The shared file-content redactor counts UTF-8 bytes with surrogate preservation,
so lone surrogates cannot discard a tool event in any adapter. The shared OMP
normalizer treats all decoder `ValueError`s as unparseable executed payloads,
retaining its existing failed-tool identity. Unexpected privacy-projection
exceptions replace the payload with a fixed opaque placeholder without retrying;
native identity and device wrapper fields remain intact.
Final tool-call envelope encoding is protected too. If encoding fails, the
record keeps its event name and ID, scalar `path`/`file_path`/`target` fields
and an opaque arguments placeholder. Other private or nested fields are
discarded; enabled-native turn effects remain unchanged.

## Server prompt outcomes

`ACPServer.prompt` settles on the first terminal outcome for its room:
Expand Down
14 changes: 14 additions & 0 deletions docs/ci-cd-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ the workflow files cannot say for themselves.
(`ci.yml`, `pr-title.yml`, `release-gate.yml` and `python-core-coverage.yml` have one).
Without it the queue waits forever for a check that never starts.

## Private Core coverage

- `python-core-coverage.yml` checks out the private Core repository with
`CORE_SDK_READ_KEY`. Fork pull requests and Dependabot cannot receive that secret,
so this job skips those contexts rather than attempting an unauthenticated checkout.
- The ordinary CI test matrix still runs for fork pull requests. Core coverage runs
for trusted pull requests, merge groups, scheduled runs, and manual dispatches.
- To validate a fork revision before merge, a maintainer can publish its reviewed
commit to a temporary upstream branch and run
`gh workflow run python-core-coverage.yml --repo band-ai/band-sdk-python --ref <branch>`.
Check that the run's head SHA matches the reviewed commit, wait for coverage and
reporting to finish, then remove only that temporary branch. The manual workflow
also posts its normal coverage digest; no secrets should be exposed to fork code.

## PyPI publish

- **Trusted publishing binds to the top-level filename `band-publish.yml`** (with environment
Expand Down
Loading
Loading