Skip to content
Draft
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
4 changes: 3 additions & 1 deletion .github/scripts/setup-omp.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
# Install Bun and pin OMP (@oh-my-pi/pi-coding-agent) for the `backends` e2e lane.
# One global binary serves both the native `omp` and the ACP-bridged `omp_acp` cells.
set -euo pipefail

OMP_PACKAGE="${OMP_PACKAGE:-@oh-my-pi/pi-coding-agent@18.2.8}"
OMP_PACKAGE="${OMP_PACKAGE:-@oh-my-pi/pi-coding-agent@18.8.9}"
OMP_MIN_BUN="${OMP_MIN_BUN:-1.3.14}"

if ! command -v bun >/dev/null 2>&1; then
Expand Down Expand Up @@ -32,5 +33,6 @@ 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 @@ -292,7 +292,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, omp_acp)
if: matrix.lane == 'backends'
run: bash .github/scripts/setup-omp.sh

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ 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/) |
| 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`, the Copilot, Cursor and OMP backends built on it, and the native `OmpAdapter` accept them.

| Category | Tool Names | What They Enable |
| ------------ | ---------- | ---------------- |
Expand Down
94 changes: 94 additions & 0 deletions docs/acp.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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`.
The native OMP adapter does not use ACP; see [OMP native RPC](#omp-native-rpc).

Client-side automatic idle release is unavailable pending provider-specific
safety qualification; see [idle room resource retention](idle-room-release.md).
Expand Down Expand Up @@ -235,3 +236,96 @@ failure fails that room turn visibly instead of falling back.
credential-redacted; values under credential-named fields are redacted in full.
- The ACP Python client drains queued update handlers before surfacing a prompt error, so
a slow client handler can still delay its own `prompt()`.

## 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 `band.integrations.ompprocess.OMP_NATIVE_PINNED_PACKAGE`
or later (Bun 1.3.14 or later). The `omp` extra does not require the ACP runtime,
and the adapter exists beside the ACP-bridged `OmpACPAdapter`; the CI backends lane
installs one shared `omp` binary at the native pin for both. 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` are constructor callables.
The native adapter has no 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
shared 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. Replayed history is identified as previously handled data; it is not a
new participant handoff.
- **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. `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.ompprocess`. 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 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. The enclosing turn timeout still bounds startup.
- **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. Native
names and turn effects still use enabled capabilities plus contacts; withheld
device writes retain their path, other fields and string payload. Unparseable
arguments 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. If encoding a tool-call envelope
fails, the record keeps its event name and ID, scalar `path`/`file_path`/`target`
fields and an opaque arguments placeholder.
4 changes: 4 additions & 0 deletions docs/idle-room-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ specific qualification blocker. This also applies to JSON/YAML configuration and
Codex environment settings. There is no supported bypass or restricted feature
profile.

`OmpAdapterConfig` (native OMP) has no `retention` setting. Native OMP turn
settlement gates Band message completion, not automatic idle process release; room
processes stay resident until explicit cleanup, a failed turn, or cancellation.

```python
from pydantic import ValidationError

Expand Down
87 changes: 87 additions & 0 deletions examples/omp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# /// script
# requires-python = ">=3.11"
# dependencies = ["band-sdk[omp]>=4.0.0"]
# ///
"""
OMP native RPC client — bridge Band rooms to ``omp --mode rpc``.

The SDK owns one process per room, registers Band tools as native host tools,
and waits through background continuations until the session is settled.
Provider credentials are passed only to the OMP child process environment.
"""

from __future__ import annotations

import asyncio
import logging

from dotenv import load_dotenv
from pydantic_settings import BaseSettings, SettingsConfigDict

from band import Agent, configure_logging
from band.adapters import OmpAdapter, OmpAdapterConfig
from band.integrations.omp import (
DEFAULT_OMP_MODEL,
omp_model_provider,
omp_provider_api_key_env,
)

configure_logging(level=logging.INFO, root_level=logging.INFO)
logger = logging.getLogger(__name__)


class Settings(BaseSettings):
model_config = SettingsConfigDict(
extra="ignore", case_sensitive=False, env_ignore_empty=True
)

omp_agent_cwd: str = "."
omp_model: str = DEFAULT_OMP_MODEL
gemini_api_key: str = ""
anthropic_api_key: str = ""
openai_api_key: str = ""


def _provider_api_key(settings: Settings) -> str:
# omp_provider_api_key_env is the SDK's single source of truth for which
# providers OMP supports; this example only wires up credentials for the
# three most common ones, so an OMP-supported-but-unwired provider gets an
# actionable message instead of being misreported as unsupported by OMP.
env_key = omp_provider_api_key_env(omp_model_provider(settings.omp_model))
match env_key:
case "GEMINI_API_KEY":
return settings.gemini_api_key
case "ANTHROPIC_API_KEY":
return settings.anthropic_api_key
case "OPENAI_API_KEY":
return settings.openai_api_key
case _:
raise ValueError(
f"{settings.omp_model!r} needs {env_key}, which this example "
"doesn't read; add a field for it to Settings, or use "
"anthropic/google/openai"
)


async def main() -> None:
load_dotenv()
settings = Settings()
api_key = _provider_api_key(settings)
if not api_key:
raise ValueError("Set the provider API key env var matching OMP_MODEL")

config = OmpAdapterConfig(
cwd=settings.omp_agent_cwd,
model=settings.omp_model,
api_key=api_key,
inject_band_tools=True,
)
adapter = OmpAdapter(config)

logger.info("Starting OMP native RPC bridge...")
async with Agent.from_config("omp_agent", adapter=adapter) as agent:
await agent.run_forever()


if __name__ == "__main__":
asyncio.run(main())
5 changes: 5 additions & 0 deletions src/band/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
uv add band-sdk[codex]
uv add band-sdk[google_adk]
uv add band-sdk[opencode]
uv add band-sdk[omp]
uv add band-sdk[slack]
uv add band-sdk[strands]
uv add band-sdk[nooa]
Expand Down Expand Up @@ -120,6 +121,9 @@
from band.adapters.nooa import NooaAdapter as NooaAdapter
from band.adapters.nooa import NooaAdapterConfig as NooaAdapterConfig
from band.adapters.nooa import NooaRoomAgent as NooaRoomAgent
from band.adapters.omp import OmpAdapter as OmpAdapter
from band.adapters.omp import OmpAdapterConfig as OmpAdapterConfig
from band.adapters.omp import OmpPermissionRequest as OmpPermissionRequest
from band.adapters.omp_acp import (
OmpACPAdapter as OmpACPAdapter,
)
Expand Down Expand Up @@ -172,6 +176,7 @@
copilot_acp=["CopilotACPAdapter", "CopilotACPAdapterConfig"],
cursor_acp=["CursorACPAdapter", "CursorACPAdapterConfig"],
omp_acp=["OmpACPAdapter", "OmpACPAdapterConfig"],
omp=["OmpAdapter", "OmpAdapterConfig", "OmpPermissionRequest"],
parlant=["ParlantAdapter", "ParlantAdapterConfig"],
crewai=["CrewAIAdapter", "CrewAIAdapterConfig"],
crewai_flow=["CrewAIFlowAdapter", "CrewAIFlowAdapterConfig"],
Expand Down
Loading
Loading