import { type InboundMediaOptions } from "./inbound-media.js"; import type Relay from "@relaymessenger/sdk"; import { ClientSideConnection, type PermissionOption, type RequestPermissionRequest, type RequestPermissionResponse, type McpServer, type AuthMethod } from "@agentclientprotocol/sdk"; import type { AcpSessionStore } from "./acp-threads.js"; import { type HostedMcp } from "./hosted-mcp.js"; import { type ApprovalChoice, type ApprovalOutcome, type OwnerApprovals } from "./approvals.js"; /** * What `relay connect cursor|gemini-cli|cline|opencode` leaves running so the * agent answers Relay messages by itself. * * These agents reach Relay through MCP, and MCP cannot start a turn, so a * message waits until somebody asks the agent to read it. This is the process * that asks. It speaks the Agent Client Protocol (ACP) — the same protocol Zed * uses to drive Cursor, Gemini CLI and OpenCode in the background — over the * official `@agentclientprotocol/sdk`. It runs the agent's own ACP command * (`cursor-agent acp`, `gemini --experimental-acp`, `opencode acp`), gives every * chat its own ACP session, and sends the final answer back to the same chat. * * Relay's own tools travel through the session, natively: Relay's hosted MCP * server, with this agent's token, is handed to `session/new` instead of being * written into the agent's `mcp.json`, so the agent keeps Relay's send, read and react tools * while this process drives its turns. This mirrors `codex-bridge.ts`, which * does the same over Codex's `app-server`; the two never share a session file. */ /** Relay takes 1 to 255 characters for an idempotency key (contracts/relay-v1-openapi.yaml). */ export declare const replyKey: (eventId: string) => string; /** What the agent is told this client is (`clientInfo`, InitializeRequest). */ export declare const CLIENT_NAME = "relaymessenger"; /** * One message, as the prompt the agent is given. The agent keeps its Relay * tools during the turn, so the prompt says who answers the person: this * process sends the final message, and the agent must not send a second one. * The same words as `codexPrompt` (codex-bridge.ts), so both bridges say the * same thing. */ export declare const acpPrompt: (sender: string, text: string, timezone?: string) => string; /** What to run for the agent: the file, and the words that put it in ACP mode. */ export interface AcpCommand { /** The executable, e.g. `cursor-agent`, or the absolute path connect found. */ command: string; /** The ACP sub-command and flags, e.g. `["acp"]` or `["--experimental-acp"]`. */ args: readonly string[]; } /** * The file to run for the agent. connect hands over the one its own sniff found * on PATH, and the bare name when it found none (connect.ts). Windows has no * file called `cursor-agent`: npm installs a `.cmd` shim, so a bare name is * looked up again here and, failing that, left with the extension `cmd.exe` can * find. The ACP words are kept as they are (runtime-sniff.ts, spawn-command.ts). */ export declare const acpCommand: (found: string, args: readonly string[], env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform) => Promise; /** * Relay's hosted MCP server as ACP takes it. HTTP is optional in ACP: a client * sends an `McpServerHttp` (`type: "http"`, `name`, `url`, `headers` as * `{ name, value }` pairs) only to an agent whose `initialize` answer carries * `mcpCapabilities.http`, and every agent must take stdio (ACP "Session Setup", * MCP Servers; _sources/mcp-hosted-docs-20260926/acp-session-setup.md:374-470; * `McpServerHttp`, `McpCapabilities` in @agentclientprotocol/sdk 1.4.0 * schema/types.gen). An agent without it gets the stdio `mcp-remote` entry * Relay-Docs gives clients without remote support (hosted-mcp.ts). */ export declare const relayMcpServer: (mcp: HostedMcp, capabilities: { http?: boolean; } | null | undefined) => McpServer; /** * Whether a permission request is for one of Relay's own tools, which run * without a card, as Inkbox's own tools do (inkbox-claude-code-plugin- * sessions.py.txt:1171-1173). ACP's `ToolCall` names no MCP server * (acp-protocol-tool-calls.md), so this reads the one agent whose naming is * known: Gemini CLI names an MCP tool `mcp_{server}_{tool}` * (packages/core/src/tools/mcp-tool.ts `generateValidName`) and starts each * call id with the tool's name (packages/cli/src/acp/acpSession.ts * `generateCallId`; _sources/oss-20260912/gemini-cli). Any other request gets * a card, the side that asks rather than allows. */ export declare const isRelayToolCall: (params: Pick) => boolean; /** * The agent's own options mapped to the card's three: its first * `allow_once`, its first `allow_always` as "for this session" (Gemini CLI * names it "Allow for this session", acpUtils.ts `toPermissionOptions`), and * its first `reject_once`, else `reject_always`. Each keeps the agent's own * name and id (`PermissionOption`, acp-protocol-tool-calls.md). */ export declare const acpChoices: (options: readonly PermissionOption[]) => ApprovalChoice[]; /** * The answer the agent gets. A cancelled turn is answered `cancelled`, which * ACP requires ("If the current prompt turn gets cancelled, the Client MUST * respond with the "cancelled" outcome", acp-protocol-tool-calls.md:193). A * prompt nobody allowed selects the agent's own reject option; ACP's answer * carries no message. */ export declare const acpPermissionResponse: (params: Pick, outcome: ApprovalOutcome) => RequestPermissionResponse; /** Relay's own secrets a person may hold in the shell that runs connect. */ export declare const RELAY_SECRET_ENV: readonly ["RELAY_AGENT_TOKEN", "RELAY_WEBHOOK_SECRET"]; /** * The agent's environment: this process's own, without Relay's secrets. The * agent reaches Relay through the MCP server handed to its session, which * carries the token itself (`relayMcpServer`), so nothing the model runs needs * it. The Codex bridge keeps the same name out of every command Codex runs * (`shell_environment_policy`, codex-bridge.ts). * * Everything else passes through, the client's own sign-in variables above * all: Gemini CLI reads `GEMINI_API_KEY` (docs/reference/configuration.md; see * `ENV_AUTH_METHODS`), and Cline reads `CLINE_API_KEY`, "Authenticate with an * API key instead of interactive sign-in" (Cline docs, usage/acp; * _sources/unattended-agent-permissions-20260926/cline-docs-usage-acp.mdx.txt:131), * and Cursor's agent reads `CURSOR_API_KEY` (Cursor docs, cli/headless; * cursor-com-docs-cli-headless.txt:13). Without them the agent cannot sign in * and answers nothing. */ export declare const acpEnvironment: (env: NodeJS.ProcessEnv) => NodeJS.ProcessEnv; /** * One permission request, answered by the agent's owners in Relay; Relay's * own tools run without asking. */ export declare const acpPermission: (label: string, approvals: Pick) => (params: RequestPermissionRequest, signal?: AbortSignal) => Promise; /** * How long a loaded session must stay silent before the bridge prompts it. * ACP says an agent replays the whole conversation and only then answers * `session/load` (Session Setup, "Loading Sessions"; * _sources/mcp-hosted-docs-20260926/acp-session-setup.md:137,181). Gemini CLI * answers first and replays after (`session.streamHistory` is not awaited, * acpSessionManager.ts `loadSession`; acpSession.ts:241), so without a wait the * old answers stream into the new turn's answer (the Daytona run of * 2026-09-26 sent "Probe a8749ac6Probe a8749ac6"). An agent that follows the * spec sends nothing after its answer and costs one window. */ export declare const REPLAY_QUIET_MS = 300; export declare const REPLAY_MAX_MS = 10000; /** ACP's `auth_required` error code (`RequestError.authRequired`, jsonrpc.js). */ export declare const AUTH_REQUIRED = -32000; /** * Sign-in methods whose credential is an environment variable the agent reads * itself, by method id. ACP leaves the choice of method to the client * (Authentication, "Protocol-driven authentication"; * _sources/mcp-hosted-docs-20260926/acp-v1-authentication.md:138-170), and no * person is at this client to choose, so the bridge picks only a method whose * credential is already present, the rule acpx follows (`selectAuthMethod`, * _sources/network-20260926/mcp-xos/acpx-src-src_acp_client.ts.txt:2054-2103). * `gemini-api-key` is Gemini CLI's `AuthType.USE_GEMINI`, read from * `GEMINI_API_KEY` (Gemini CLI docs/reference/configuration.md; Zed hands the * same variable to Gemini, zed-agent_servers-custom.rs.txt:250-253). */ export declare const ENV_AUTH_METHODS: Readonly>; /** The advertised method whose credential this environment holds, when there is one. */ export declare const authMethodFromEnv: (methods: readonly AuthMethod[] | undefined, env: NodeJS.ProcessEnv) => string | undefined; /** An ACP error, an Error, or anything thrown, as one line. */ export declare const acpFailure: (error: unknown) => string; /** One running ACP agent, and the calls this bridge makes to it. */ export interface AcpAgent { client: Pick; /** * Collect the answer streamed for one session's running turn. Every * `agent_message_chunk` of text is handed to the sink until the returned * function is called. Turns are serial within a session, so at most one sink * is registered per session at a time. */ collect(sessionId: string, sink: (text: string) => void): () => void; /** * Resolves once the agent has sent nothing for this session for `windowMs`, * or after `maxMs` at most. See `REPLAY_QUIET_MS`. */ quiet(sessionId: string, windowMs?: number, maxMs?: number): Promise; /** True once the agent advertised `session/load`; set after `initialize`. */ canLoad: boolean; /** What the agent said it can reach an MCP server over; set after `initialize`. */ mcp?: { http?: boolean; } | null; /** The sign-in methods the agent advertised in `initialize`. */ authMethods?: readonly AuthMethod[]; /** Set once `authenticate` has been sent, so it is sent at most once per process. */ authenticated?: boolean; /** Resolves, with the line to show the person, when the process is gone. */ stopped: Promise; /** True once the process is gone, so the next message starts a new one. */ gone(): boolean; } /** Starts the agent's ACP command and drives it over the official SDK. */ export declare const startAcpAgent: (acp: AcpCommand, cwd: string, signal: AbortSignal, /** Answers `session/request_permission`; `acpPermission` in the bridge. */ permission: (params: RequestPermissionRequest) => Promise) => AcpAgent; /** The turn the agent is running for one chat right now. */ export interface LiveTurn { sessionId: string; /** A newer message for this chat replaced it, so its answer is not sent. */ dropped: boolean; } /** How a turn ended (`StopReason`, schema/types.gen), and what it said. */ export interface TurnOutcome { /** `end_turn`, `cancelled`, `refusal`, `max_tokens` or `max_turn_requests`. */ stopReason: string; answer: string; } /** * Runs one turn in one session, and gives back what the agent answered. * * The answer is every `agent_message_chunk` of text the agent streamed before * `session/prompt` returned its stop reason, joined in order. */ export declare const runTurn: (agent: AcpAgent, input: { sessionId: string; prompt: string; onStarted(turn: LiveTurn): void; }) => Promise; export interface AcpBridgeInput { client: Pick; media?: Omit; /** The agent's ACP command, and the folder to run it in. */ acp: AcpCommand; cwd: string; /** Relay's hosted MCP server and this agent's token, handed to every * session so Relay's tools travel with it. */ mcp: HostedMcp; /** The label shown to the person, e.g. "Cursor". */ label: string; /** Asks the agent's owners, in Relay, whatever the agent would ask its editor. */ approvals: Pick; /** Which ACP session belongs to which chat, across restarts. */ sessions: AcpSessionStore; signal: AbortSignal; /** One line to the terminal the person is watching. */ say(line: string): void; /** Where a sign-in method's credential is looked for; this process's own by default. */ env?: NodeJS.ProcessEnv; } /** * Answers every message that arrives until the signal stops it. * * Turns run one at a time inside a chat and at the same time across chats, * which is what one agent with one session per chat allows. A person's * message for a chat whose turn for a person is still running cancels that * turn (`session/cancel`) and takes its place; the cancelled turn sends * nothing. An agent's message waits for the running turn instead * (`replacesLiveTurn`). */ export declare const runAcpBridge: (input: AcpBridgeInput) => Promise; //# sourceMappingURL=acp-bridge.d.ts.map