| import type { SDKMessage } from '../entrypoints/agentSdkTypes.js' |
| import type { |
| SDKControlCancelRequest, |
| SDKControlPermissionRequest, |
| SDKControlRequest, |
| SDKControlResponse, |
| } from '../entrypoints/sdk/controlTypes.js' |
| import { logForDebugging } from '../utils/debug.js' |
| import { logError } from '../utils/log.js' |
| import { |
| type RemoteMessageContent, |
| sendEventToRemoteSession, |
| } from '../utils/teleport/api.js' |
| import { |
| SessionsWebSocket, |
| type SessionsWebSocketCallbacks, |
| } from './SessionsWebSocket.js' |
|
|
| |
| |
| |
| function isSDKMessage( |
| message: |
| | SDKMessage |
| | SDKControlRequest |
| | SDKControlResponse |
| | SDKControlCancelRequest, |
| ): message is SDKMessage { |
| return ( |
| message.type !== 'control_request' && |
| message.type !== 'control_response' && |
| message.type !== 'control_cancel_request' |
| ) |
| } |
|
|
| |
| |
| |
| |
| export type RemotePermissionResponse = |
| | { |
| behavior: 'allow' |
| updatedInput: Record<string, unknown> |
| } |
| | { |
| behavior: 'deny' |
| message: string |
| } |
|
|
| export type RemoteSessionConfig = { |
| sessionId: string |
| getAccessToken: () => string |
| orgUuid: string |
| |
| hasInitialPrompt?: boolean |
| |
| |
| |
| |
| |
| viewerOnly?: boolean |
| } |
|
|
| export type RemoteSessionCallbacks = { |
| |
| onMessage: (message: SDKMessage) => void |
| |
| onPermissionRequest: ( |
| request: SDKControlPermissionRequest, |
| requestId: string, |
| ) => void |
| |
| onPermissionCancelled?: ( |
| requestId: string, |
| toolUseId: string | undefined, |
| ) => void |
| |
| onConnected?: () => void |
| |
| onDisconnected?: () => void |
| |
| onReconnecting?: () => void |
| |
| onError?: (error: Error) => void |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export class RemoteSessionManager { |
| private websocket: SessionsWebSocket | null = null |
| private pendingPermissionRequests: Map<string, SDKControlPermissionRequest> = |
| new Map() |
|
|
| constructor( |
| private readonly config: RemoteSessionConfig, |
| private readonly callbacks: RemoteSessionCallbacks, |
| ) {} |
|
|
| |
| |
| |
| connect(): void { |
| logForDebugging( |
| `[RemoteSessionManager] Connecting to session ${this.config.sessionId}`, |
| ) |
|
|
| const wsCallbacks: SessionsWebSocketCallbacks = { |
| onMessage: message => this.handleMessage(message), |
| onConnected: () => { |
| logForDebugging('[RemoteSessionManager] Connected') |
| this.callbacks.onConnected?.() |
| }, |
| onClose: () => { |
| logForDebugging('[RemoteSessionManager] Disconnected') |
| this.callbacks.onDisconnected?.() |
| }, |
| onReconnecting: () => { |
| logForDebugging('[RemoteSessionManager] Reconnecting') |
| this.callbacks.onReconnecting?.() |
| }, |
| onError: error => { |
| logError(error) |
| this.callbacks.onError?.(error) |
| }, |
| } |
|
|
| this.websocket = new SessionsWebSocket( |
| this.config.sessionId, |
| this.config.orgUuid, |
| this.config.getAccessToken, |
| wsCallbacks, |
| ) |
|
|
| void this.websocket.connect() |
| } |
|
|
| |
| |
| |
| private handleMessage( |
| message: |
| | SDKMessage |
| | SDKControlRequest |
| | SDKControlResponse |
| | SDKControlCancelRequest, |
| ): void { |
| |
| if (message.type === 'control_request') { |
| this.handleControlRequest(message) |
| return |
| } |
|
|
| |
| if (message.type === 'control_cancel_request') { |
| const { request_id } = message |
| const pendingRequest = this.pendingPermissionRequests.get(request_id) |
| logForDebugging( |
| `[RemoteSessionManager] Permission request cancelled: ${request_id}`, |
| ) |
| this.pendingPermissionRequests.delete(request_id) |
| this.callbacks.onPermissionCancelled?.( |
| request_id, |
| pendingRequest?.tool_use_id, |
| ) |
| return |
| } |
|
|
| |
| if (message.type === 'control_response') { |
| logForDebugging('[RemoteSessionManager] Received control response') |
| return |
| } |
|
|
| |
| if (isSDKMessage(message)) { |
| this.callbacks.onMessage(message) |
| } |
| } |
|
|
| |
| |
| |
| private handleControlRequest(request: SDKControlRequest): void { |
| const { request_id, request: inner } = request |
|
|
| if (inner.subtype === 'can_use_tool') { |
| logForDebugging( |
| `[RemoteSessionManager] Permission request for tool: ${inner.tool_name}`, |
| ) |
| this.pendingPermissionRequests.set(request_id, inner) |
| this.callbacks.onPermissionRequest(inner, request_id) |
| } else { |
| |
| |
| logForDebugging( |
| `[RemoteSessionManager] Unsupported control request subtype: ${inner.subtype}`, |
| ) |
| const response: SDKControlResponse = { |
| type: 'control_response', |
| response: { |
| subtype: 'error', |
| request_id, |
| error: `Unsupported control request subtype: ${inner.subtype}`, |
| }, |
| } |
| this.websocket?.sendControlResponse(response) |
| } |
| } |
|
|
| |
| |
| |
| async sendMessage( |
| content: RemoteMessageContent, |
| opts?: { uuid?: string }, |
| ): Promise<boolean> { |
| logForDebugging( |
| `[RemoteSessionManager] Sending message to session ${this.config.sessionId}`, |
| ) |
|
|
| const success = await sendEventToRemoteSession( |
| this.config.sessionId, |
| content, |
| opts, |
| ) |
|
|
| if (!success) { |
| logError( |
| new Error( |
| `[RemoteSessionManager] Failed to send message to session ${this.config.sessionId}`, |
| ), |
| ) |
| } |
|
|
| return success |
| } |
|
|
| |
| |
| |
| respondToPermissionRequest( |
| requestId: string, |
| result: RemotePermissionResponse, |
| ): void { |
| const pendingRequest = this.pendingPermissionRequests.get(requestId) |
| if (!pendingRequest) { |
| logError( |
| new Error( |
| `[RemoteSessionManager] No pending permission request with ID: ${requestId}`, |
| ), |
| ) |
| return |
| } |
|
|
| this.pendingPermissionRequests.delete(requestId) |
|
|
| const response: SDKControlResponse = { |
| type: 'control_response', |
| response: { |
| subtype: 'success', |
| request_id: requestId, |
| response: { |
| behavior: result.behavior, |
| ...(result.behavior === 'allow' |
| ? { updatedInput: result.updatedInput } |
| : { message: result.message }), |
| }, |
| }, |
| } |
|
|
| logForDebugging( |
| `[RemoteSessionManager] Sending permission response: ${result.behavior}`, |
| ) |
|
|
| this.websocket?.sendControlResponse(response) |
| } |
|
|
| |
| |
| |
| isConnected(): boolean { |
| return this.websocket?.isConnected() ?? false |
| } |
|
|
| |
| |
| |
| cancelSession(): void { |
| logForDebugging('[RemoteSessionManager] Sending interrupt signal') |
| this.websocket?.sendControlRequest({ subtype: 'interrupt' }) |
| } |
|
|
| |
| |
| |
| getSessionId(): string { |
| return this.config.sessionId |
| } |
|
|
| |
| |
| |
| disconnect(): void { |
| logForDebugging('[RemoteSessionManager] Disconnecting') |
| this.websocket?.close() |
| this.websocket = null |
| this.pendingPermissionRequests.clear() |
| } |
|
|
| |
| |
| |
| |
| reconnect(): void { |
| logForDebugging('[RemoteSessionManager] Reconnecting WebSocket') |
| this.websocket?.reconnect() |
| } |
| } |
|
|
| |
| |
| |
| export function createRemoteSessionConfig( |
| sessionId: string, |
| getAccessToken: () => string, |
| orgUuid: string, |
| hasInitialPrompt = false, |
| viewerOnly = false, |
| ): RemoteSessionConfig { |
| return { |
| sessionId, |
| getAccessToken, |
| orgUuid, |
| hasInitialPrompt, |
| viewerOnly, |
| } |
| } |
|
|