microsoft / microsoft/agent-host-protocol
Proposal: Client-Executed Terminals — let terminals run pty processes on clients
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 344
- Forks
- 122
- Avg merge
- 14h 12m
- Merged PRs (30d)
- 22
Description
Proposal: Client-Executed Terminals — let terminals run pty processes on clients
Disclaimer: This proposal was developed with help from an LLM, although it stems from a real experienced implementation constraint. It was reviewed by a human and leaves some parts underspecified in the interest of explaining the concept.
Summary
Extend the terminal channel to support terminals whose pty processes execute on a client rather than the server. The server remains the coordination hub and source of truth for terminal state; the pty-owning client produces output that the server distributes to all subscribers. For non-owning clients and agents, client-executed terminals are observation-only — they can subscribe and receive output but cannot send input, resize, or clear.
Motivation
Today, AHP terminals assume the server owns every pty. This works when the agent host has direct access to the shell — local development, containers, remote VMs. But many setups don't:
- IDE-connected agents. A VS Code extension wants to use the terminals the user already has open — with their environment, credentials, and working directory. Duplicating that environment on the server is impractical.
- Thin agent hosts. The agent runs remotely, but the user's workspace is their local machine. The only pty available is on the client side.
- Shared terminal visibility. A client has an existing terminal running a dev server. The agent and other clients should be able to observe it.
In all these cases the pty lives on a client, but coordination — who owns it, who sees output — still needs to flow through the server.
Proposal
Four small changes:
1. executionTarget on createTerminal
interface CreateTerminalParams extends BaseParams {
channel: URI;
claim: TerminalClaim;
name?: string;
cwd?: URI;
cols?: number;
rows?: number;
executionTarget?: 'server' | 'client'; // default: 'server'
}
When executionTarget: 'client':
- The server does not spawn a pty. It allocates the terminal resource, records the claim, and dispatches
root/terminalsChanged. - The claim MUST be a client claim (
kind: 'client'). Session claims are rejected — there is no client to own the pty. - The claiming client creates a local pty and produces output via
terminal/output.
When executionTarget: 'server' (or omitted): behavior is identical to today.
2. terminal/output — new client-dispatchable action
interface TerminalOutputAction {
type: ActionType.TerminalOutput;
data: string;
}
Direction: client → server
Server behavior on receipt:
- Validate the dispatching client holds the terminal's claim. Reject otherwise.
- Apply the data to the terminal's state identically to
terminal/data(append to the tail content part). - Re-broadcast as
terminal/datato all other subscribers.
Why a separate action instead of reusing terminal/data? terminal/data is server-only. Making it bidirectional would change the semantics of every reducer that handles it.
3. terminal/exited becomes client-dispatchable for client-executed terminals
Today terminal/exited is server-only. For client-executed terminals, the owning client dispatches terminal/exited with an exitCode when its local pty exits. The server validates the sender holds the claim, applies it to state, and re-broadcasts to all subscribers.
4. executionTarget on TerminalInfo and TerminalState
Add executionTarget?: 'server' | 'client' to both. Informational — clients can use it to adjust behavior (e.g., show a "local" vs. "remote" indicator).
Wire flows
Server-executed terminal (unchanged)
Client A → createTerminal { executionTarget: 'server' } → Server
Server spawns pty, dispatches root/terminalsChanged
Client A → subscribe ahp-terminal:/<id>
Server → terminal/data { data: "ls\r\nfile1 file2\r\n$" } → Client A
Client A → terminal/input { data: "echo hello\r" } → Server
Server writes to pty
Server → terminal/data { data: "echo hello\r\nhello\r\n$" } → Client A
Client-executed terminal (new)
Client A → createTerminal { executionTarget: 'client', claim: { kind: 'client', clientId: 'client-a' } } → Server
Server allocates resource, dispatches root/terminalsChanged
Client A creates local pty, subscribes to ahp-terminal:/<id>
# Client A types locally — writes to pty directly, no server round-trip:
Client A pty produces output
Client A → terminal/output { data: "echo hello\r\nhello\r\n$" } → Server
Server applies, re-broadcasts as terminal/data → other subscribers
# Agent reads output:
Agent subscribes to ahp-terminal:/<id>
Agent → terminal/data { data: "echo hello\r\nhello\r\n$" } (received automatically)
# Agent attempts to send input — rejected:
Agent → terminal/input { data: "ls\r" } → Server
Server → error (client-executed terminals are observation-only for non-owners)
Disconnection and disposal
If the owning client disconnects, the server dispatches terminal/exited with exit code -1 and removes the terminal from the catalogue. This is identical to the server killing a pty for a server-executed terminal whose process died.
disposeTerminal from any authorized client (or the agent) removes the terminal from the catalogue. The server sends terminal/exited to the owning client, which kills its local pty.
Authorization
For client-executed terminals, only the owning client (origin.clientId === claim.clientId) may dispatch actions — terminal/output, terminal/exited, terminal/cleared, terminal/resized. All other clients and agents are observation-only: they may subscribe and receive terminal/data but any attempt to dispatch terminal/input, terminal/cleared, or terminal/resized is rejected by the server. The server can also dispatch terminal/exited itself (e.g., on client disconnect or disposeTerminal).
Open questions
- Capability advertisement. Should the server advertise support for
executionTarget: 'client'inInitializeResultorAgentInfo, or is it always available? - Command detection for client-executed terminals. The owning client may produce
terminal/commandExecuted/terminal/commandFinishedlocally if its shell has integration. Should the server accept these from the owning client, making command detection available? Or is it simply unavailable in v1? executionTargetimmutability. ShouldexecutionTargetbe immutable after creation? The proposal implies it is (it's oncreateTerminal, not a dispatchable action), but this should be stated explicitly.createTerminalrejection. What error code should the server return whenexecutionTarget: 'client'is paired with a session claim?
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing the terminal channel definitions for CreateTerminalParams, TerminalOutputAction, TerminalInfo, TerminalState, and the terminal actions described in the proposal. Resolve the listed authorization, capability, command-detection, immutability, and rejection-error questions, then document and implement the agreed protocol behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100