Overview
The ACP SDK allows agents to create terminals and execute commands in the client’s environment. This enables agents to run build tools, tests, scripts, and other command-line operations.
Terminal operations are only available if the client advertises the terminal capability during initialization.
Creating Terminals
The createTerminal method executes a command in a new terminal:
async createTerminal (
params : CreateTerminalRequest
): Promise < TerminalHandle >
The session ID creating the terminal
Optional command arguments
Optional working directory for the command
Optional environment variables
A handle to control and monitor the terminal
Returns a TerminalHandle that can be used to get output, wait for exit, kill the command, or release the terminal.
Example
class MyAgent implements acp . Agent {
private connection : acp . AgentSideConnection ;
async runCommand (
sessionId : string ,
command : string ,
args : string [] = []
) : Promise < string > {
// Create the terminal
const terminal = await this . connection . createTerminal ({
sessionId ,
command ,
args ,
workingDirectory: "/home/user/project" ,
});
try {
// Wait for the command to complete
const exitStatus = await terminal . waitForExit ();
// Get the final output
const output = await terminal . currentOutput ();
if ( exitStatus . exitCode === 0 ) {
return output . output ;
} else {
throw new Error ( `Command failed with exit code ${ exitStatus . exitCode } ` );
}
} finally {
// Always release the terminal
await terminal . release ();
}
}
}
TerminalHandle Class
The TerminalHandle class provides methods to control and monitor a terminal:
export class TerminalHandle {
id : string ;
async currentOutput () : Promise < TerminalOutputResponse >;
async waitForExit () : Promise < WaitForTerminalExitResponse >;
async kill () : Promise < KillTerminalResponse >;
async release () : Promise < ReleaseTerminalResponse | void >;
[ Symbol . asyncDispose ]() : Promise < void >;
}
currentOutput
Gets the current terminal output without waiting for the command to exit.
async currentOutput (): Promise < TerminalOutputResponse >
The current output from the terminal
The exit code if the command has already exited
The signal that terminated the process, if applicable
Example
const terminal = await this . connection . createTerminal ({
sessionId ,
command: "npm" ,
args: [ "install" ],
});
// Poll for output while command is running
const pollInterval = setInterval ( async () => {
const output = await terminal . currentOutput ();
console . log ( "Current output:" , output . output );
if ( output . exitCode !== undefined ) {
clearInterval ( pollInterval );
}
}, 1000 );
waitForExit
Waits for the terminal command to complete and returns its exit status.
async waitForExit (): Promise < WaitForTerminalExitResponse >
The exit code of the process
The signal that terminated the process, if applicable
This method blocks until the command completes, providing the exit code and/or signal that terminated the process.
Example
const terminal = await this . connection . createTerminal ({
sessionId ,
command: "npm" ,
args: [ "test" ],
});
// Wait for tests to complete
const exitStatus = await terminal . waitForExit ();
if ( exitStatus . exitCode === 0 ) {
console . log ( "Tests passed!" );
} else {
console . error ( `Tests failed with exit code ${ exitStatus . exitCode } ` );
}
await terminal . release ();
kill
Kills the terminal command without releasing the terminal.
async kill (): Promise < KillTerminalResponse >
The terminal remains valid after killing, allowing you to:
Get the final output with currentOutput()
Check the exit status
Release the terminal when done
Useful for implementing timeouts or cancellation.
Example
const terminal = await this . connection . createTerminal ({
sessionId ,
command: "long-running-command" ,
});
// Set a timeout
const timeout = setTimeout ( async () => {
console . log ( "Command timed out, killing..." );
await terminal . kill ();
}, 30000 ); // 30 seconds
try {
await terminal . waitForExit ();
clearTimeout ( timeout );
} finally {
// Get final output even after killing
const output = await terminal . currentOutput ();
console . log ( "Final output:" , output . output );
await terminal . release ();
}
release
Releases the terminal and frees all associated resources.
async release (): Promise < ReleaseTerminalResponse | void >
If the command is still running, it will be killed. After release, the terminal ID becomes invalid and cannot be used with other terminal methods.
Tool calls that already reference this terminal will continue to display its output.
Always call release() when done with the terminal to free resources.
Example with await using
The TerminalHandle class supports async disposal via Symbol.asyncDispose, allowing automatic cleanup:
// Automatically releases the terminal when it goes out of scope
await using terminal = await this . connection . createTerminal ({
sessionId ,
command: "npm" ,
args: [ "build" ],
});
await terminal . waitForExit ();
const output = await terminal . currentOutput ();
// terminal.release() is called automatically
Terminals can be embedded in tool calls by using their ID in ToolCallContent with type “terminal”:
// Create a terminal
const terminal = await this . connection . createTerminal ({
sessionId ,
command: "npm" ,
args: [ "run" , "build" ],
});
// Send a tool call that references the terminal
await this . connection . sessionUpdate ({
sessionId ,
update: {
sessionUpdate: "tool_call" ,
toolCallId: "build_1" ,
title: "Building project" ,
kind: "bash" ,
status: "pending" ,
rawInput: { command: "npm run build" },
},
});
// Wait for completion
const exitStatus = await terminal . waitForExit ();
const output = await terminal . currentOutput ();
// Update tool call with terminal output
await this . connection . sessionUpdate ({
sessionId ,
update: {
sessionUpdate: "tool_call_update" ,
toolCallId: "build_1" ,
status: exitStatus . exitCode === 0 ? "completed" : "error" ,
content: [
{
type: "terminal" ,
terminalId: terminal . id ,
},
],
rawOutput: { output: output . output , exitCode: exitStatus . exitCode },
},
});
await terminal . release ();
Error Handling
Handle errors appropriately when working with terminals:
async runCommandSafely (
sessionId : string ,
command : string ,
args : string []
): Promise < { success : boolean ; output : string ; exitCode ?: number } > {
let terminal: acp . TerminalHandle | null = null ;
try {
terminal = await this . connection . createTerminal ({
sessionId ,
command ,
args ,
});
const exitStatus = await terminal . waitForExit ();
const output = await terminal . currentOutput ();
return {
success : exitStatus . exitCode === 0 ,
output : output . output ,
exitCode : exitStatus . exitCode ,
};
} catch ( error ) {
console.error( "Terminal error:" , error);
return {
success : false ,
output : error instanceof Error ? error . message : "Unknown error" ,
};
} finally {
if ( terminal ) {
await terminal . release ();
}
}
}
Complete Example
Here’s a complete example that demonstrates terminal operations:
import * as acp from "@agentprotocol/acp" ;
class BuildAgent implements acp . Agent {
private connection : acp . AgentSideConnection ;
private capabilities : acp . ClientCapabilities | null = null ;
constructor ( connection : acp . AgentSideConnection ) {
this . connection = connection ;
}
async initialize ( params : acp . InitializeRequest ) : Promise < acp . InitializeResponse > {
this . capabilities = params . clientCapabilities ;
return {
protocolVersion: acp . PROTOCOL_VERSION ,
agentCapabilities: {},
};
}
async runBuild ( sessionId : string , workdir : string ) : Promise < void > {
if ( ! this . capabilities ?. terminal ) {
throw new Error ( "Terminal capability not available" );
}
// Send initial message
await this . connection . sessionUpdate ({
sessionId ,
update: {
sessionUpdate: "agent_message_chunk" ,
content: {
type: "text" ,
text: "Starting build process..." ,
},
},
});
// Create terminal for build
await using terminal = await this . connection . createTerminal ({
sessionId ,
command: "npm" ,
args: [ "run" , "build" ],
workingDirectory: workdir ,
});
// Send tool call
await this . connection . sessionUpdate ({
sessionId ,
update: {
sessionUpdate: "tool_call" ,
toolCallId: "build_1" ,
title: "Running npm build" ,
kind: "bash" ,
status: "pending" ,
rawInput: { command: "npm run build" },
},
});
// Wait for completion
const exitStatus = await terminal . waitForExit ();
const output = await terminal . currentOutput ();
// Update tool call with results
await this . connection . sessionUpdate ({
sessionId ,
update: {
sessionUpdate: "tool_call_update" ,
toolCallId: "build_1" ,
status: exitStatus . exitCode === 0 ? "completed" : "error" ,
content: [
{
type: "terminal" ,
terminalId: terminal . id ,
},
],
rawOutput: { output: output . output , exitCode: exitStatus . exitCode },
},
});
// Send final message
const message =
exitStatus . exitCode === 0
? "Build completed successfully!"
: `Build failed with exit code ${ exitStatus . exitCode } ` ;
await this . connection . sessionUpdate ({
sessionId ,
update: {
sessionUpdate: "agent_message_chunk" ,
content: { type: "text" , text: message },
},
});
}
// ... implement other required Agent methods
}
Best Practices
Always Release Always call release() when done to free resources, or use await using
Handle Timeouts Implement timeouts using kill() to prevent commands from running indefinitely
Check Capabilities Verify the client supports terminal operations before attempting to use them
Capture Output Get final output after waitForExit() for complete command results