Skip to main content
This guide covers how to execute commands, monitor output, and manage terminal lifecycle in both agents and clients.

Overview

The ACP terminal capability allows agents to:
  • Execute shell commands in the client environment
  • Monitor command output in real-time
  • Wait for command completion
  • Kill running commands
  • Properly clean up resources

Terminal Request Types

The SDK provides these terminal-related types in lib/src/schema.dart:
  • CreateTerminalRequest - Start a new terminal with a command
  • TerminalOutputRequest - Get current output from a terminal
  • WaitForTerminalExitRequest - Wait for command completion
  • KillTerminalCommandRequest - Terminate a running command
  • ReleaseTerminalRequest - Release terminal resources

Agent Side

Agents use terminals through the client connection to execute commands in the client’s environment.

Creating a Terminal

1
Request terminal creation
2
Agents call createTerminal on the client connection:
3
import 'package:acp_dart/acp_dart.dart';

class MyAgent implements Agent {
  final AgentSideConnection _connection;
  
  Future<void> executeCommand(String sessionId) async {
    final response = await _connection.createTerminal(
      CreateTerminalRequest(
        sessionId: sessionId,
        command: 'npm',
        args: ['install', '--verbose'],
        cwd: '/path/to/project',
        env: [
          EnvVariable(name: 'NODE_ENV', value: 'production'),
        ],
      ),
    );
    
    final terminalId = response.terminalId;
    print('Created terminal: $terminalId');
  }
}
4
Monitor terminal output
5
Get output without waiting for completion:
6
final outputResponse = await _connection.terminalOutput(
  TerminalOutputRequest(
    sessionId: sessionId,
    terminalId: terminalId,
  ),
);

print('Current output: ${outputResponse.output}');

if (outputResponse.exitStatus != null) {
  print('Command exited with code: ${outputResponse.exitStatus!.exitCode}');
}
7
Wait for completion
8
Block until the command finishes:
9
final exitResponse = await _connection.waitForTerminalExit(
  WaitForTerminalExitRequest(
    sessionId: sessionId,
    terminalId: terminalId,
  ),
);

if (exitResponse.exitCode == 0) {
  print('Command completed successfully');
} else {
  print('Command failed with code: ${exitResponse.exitCode}');
}

if (exitResponse.signal != null) {
  print('Command terminated by signal: ${exitResponse.signal}');
}
10
Clean up resources
11
Always release terminals when done:
12
await _connection.releaseTerminal(
  ReleaseTerminalRequest(
    sessionId: sessionId,
    terminalId: terminalId,
  ),
);

Using TerminalHandle

The SDK provides a convenient TerminalHandle class that wraps terminal operations:
import 'package:acp_dart/acp_dart.dart';

// Create a handle after getting terminal ID
final handle = TerminalHandle(
  terminalId,
  sessionId,
  _connection._connection,  // Internal connection
);

// Use the handle for operations
final output = await handle.currentOutput();
final exitStatus = await handle.waitForExit();
await handle.kill();
await handle.release();

// Or use async disposal pattern
await handle.dispose();  // Automatically calls release()

Reporting Terminal Tool Calls

Report terminal operations to the client:
// Report terminal creation
await _connection.sessionUpdate(
  SessionNotification(
    sessionId: sessionId,
    update: ToolCallSessionUpdate(
      toolCallId: 'terminal_1',
      title: 'Running npm install',
      kind: ToolKind.execute,
      status: ToolCallStatus.inProgress,
      content: [
        TerminalToolCallContent(terminalId: terminalId),
      ],
      rawInput: {
        'command': 'npm',
        'args': ['install', '--verbose'],
      },
    ),
  ),
);

// Wait for completion
final exitStatus = await _connection.waitForTerminalExit(
  WaitForTerminalExitRequest(
    sessionId: sessionId,
    terminalId: terminalId,
  ),
);

// Get final output
final output = await _connection.terminalOutput(
  TerminalOutputRequest(
    sessionId: sessionId,
    terminalId: terminalId,
  ),
);

// Update tool call with completion
await _connection.sessionUpdate(
  SessionNotification(
    sessionId: sessionId,
    update: ToolCallUpdateSessionUpdate(
      toolCallId: 'terminal_1',
      status: exitStatus.exitCode == 0
          ? ToolCallStatus.completed
          : ToolCallStatus.failed,
      content: [
        TerminalToolCallContent(terminalId: terminalId),
      ],
      rawOutput: {
        'exitCode': exitStatus.exitCode,
        'output': output.output,
      },
    ),
  ),
);

// Clean up
await _connection.releaseTerminal(
  ReleaseTerminalRequest(
    sessionId: sessionId,
    terminalId: terminalId,
  ),
);

Implementing Command Timeouts

Kill commands that run too long:
future command execution with timeout
try {
  final exitStatus = await _connection.waitForTerminalExit(
    WaitForTerminalExitRequest(
      sessionId: sessionId,
      terminalId: terminalId,
    ),
  ).timeout(
    Duration(seconds: 30),
    onTimeout: () async {
      // Kill the command
      await _connection.killTerminal(
        KillTerminalCommandRequest(
          sessionId: sessionId,
          terminalId: terminalId,
        ),
      );
      
      // Get final output
      final output = await _connection.terminalOutput(
        TerminalOutputRequest(
          sessionId: sessionId,
          terminalId: terminalId,
        ),
      );
      
      print('Command timed out. Output: ${output.output}');
      
      throw TimeoutException('Command exceeded 30 second timeout');
    },
  );
  
  print('Command completed with exit code: ${exitStatus.exitCode}');
  
} finally {
  // Always release the terminal
  await _connection.releaseTerminal(
    ReleaseTerminalRequest(
      sessionId: sessionId,
      terminalId: terminalId,
    ),
  );
}

Client Side

Clients implement terminal operations to execute commands in their environment.

Implementing Terminal Creation

import 'dart:io';
import 'dart:convert';
import 'package:acp_dart/acp_dart.dart';

class MyClient implements Client {
  final Map<String, Process> _terminals = {};
  final Map<String, StringBuffer> _outputs = {};
  
  @override
  Future<CreateTerminalResponse> createTerminal(
    CreateTerminalRequest params,
  ) async {
    // Generate unique terminal ID
    final terminalId = _generateTerminalId();
    
    // Build environment variables
    final environment = <String, String>{};
    if (params.env != null) {
      for (final envVar in params.env!) {
        environment[envVar.name] = envVar.value;
      }
    }
    
    // Start the process
    final process = await Process.start(
      params.command,
      params.args ?? [],
      workingDirectory: params.cwd,
      environment: environment.isNotEmpty ? environment : null,
    );
    
    // Store process and capture output
    _terminals[terminalId] = process;
    _outputs[terminalId] = StringBuffer();
    
    // Capture stdout and stderr
    process.stdout.transform(utf8.decoder).listen((data) {
      _outputs[terminalId]!.write(data);
    });
    
    process.stderr.transform(utf8.decoder).listen((data) {
      _outputs[terminalId]!.write(data);
    });
    
    return CreateTerminalResponse(terminalId: terminalId);
  }
  
  String _generateTerminalId() {
    return 'term_${DateTime.now().millisecondsSinceEpoch}';
  }
}

Implementing Terminal Output

@override
Future<TerminalOutputResponse> terminalOutput(
  TerminalOutputRequest params,
) async {
  final process = _terminals[params.terminalId];
  final output = _outputs[params.terminalId];
  
  if (process == null || output == null) {
    throw RequestError.resourceNotFound('Terminal ${params.terminalId}');
  }
  
  // Check if process has exited
  TerminalExitStatus? exitStatus;
  try {
    final exitCode = await process.exitCode.timeout(
      Duration.zero,
      onTimeout: () => null,
    );
    
    if (exitCode != null) {
      exitStatus = TerminalExitStatus(exitCode: exitCode);
    }
  } catch (e) {
    // Process still running
  }
  
  return TerminalOutputResponse(
    output: output.toString(),
    exitStatus: exitStatus,
    truncated: false,
  );
}

Implementing Wait for Exit

@override
Future<WaitForTerminalExitResponse> waitForTerminalExit(
  WaitForTerminalExitRequest params,
) async {
  final process = _terminals[params.terminalId];
  
  if (process == null) {
    throw RequestError.resourceNotFound('Terminal ${params.terminalId}');
  }
  
  // Wait for process to complete
  final exitCode = await process.exitCode;
  
  return WaitForTerminalExitResponse(
    exitCode: exitCode,
    signal: null,
  );
}

Implementing Kill Terminal

@override
Future<KillTerminalCommandResponse> killTerminal(
  KillTerminalCommandRequest params,
) async {
  final process = _terminals[params.terminalId];
  
  if (process == null) {
    throw RequestError.resourceNotFound('Terminal ${params.terminalId}');
  }
  
  // Kill the process
  process.kill();
  
  return KillTerminalCommandResponse();
}

Implementing Release Terminal

@override
Future<ReleaseTerminalResponse> releaseTerminal(
  ReleaseTerminalRequest params,
) async {
  final process = _terminals.remove(params.terminalId);
  _outputs.remove(params.terminalId);
  
  if (process != null) {
    // Kill if still running
    try {
      process.kill();
    } catch (e) {
      // Process already exited
    }
  }
  
  return ReleaseTerminalResponse();
}

Best Practices

Always release terminals when done to prevent resource leaks.

Do:

✅ Always release terminals in a finally block:
try {
  final terminal = await createTerminal(...);
  // Use terminal
} finally {
  await releaseTerminal(...);
}
✅ Set appropriate timeouts for long-running commands:
await waitForExit(...).timeout(Duration(minutes: 5));
✅ Capture both stdout and stderr:
process.stdout.listen((data) => buffer.write(data));
process.stderr.listen((data) => buffer.write(data));

Don’t:

❌ Leave terminals unreleased after errors:
// Bad: terminal leaks if error occurs
final terminal = await createTerminal(...);
if (someCondition) throw Exception('Error');
await releaseTerminal(...);
❌ Execute dangerous commands without permission ❌ Block indefinitely waiting for command completion

Complete Example

See the complete client example:
dart run example/client.dart

Next Steps

File System Operations

Learn about file operations

Error Handling

Handle terminal errors gracefully

Build docs developers (and LLMs) love