| |
|
|
| import type { SDKMessage } from '../entrypoints/agentSdkTypes.js' |
| import type { |
| SDKControlPermissionRequest, |
| StdoutMessage, |
| } from '../entrypoints/sdk/controlTypes.js' |
| import type { RemotePermissionResponse } from '../remote/RemoteSessionManager.js' |
| import { logForDebugging } from '../utils/debug.js' |
| import { jsonParse, jsonStringify } from '../utils/slowOperations.js' |
| import type { RemoteMessageContent } from '../utils/teleport/api.js' |
|
|
| export type DirectConnectConfig = { |
| serverUrl: string |
| sessionId: string |
| wsUrl: string |
| authToken?: string |
| } |
|
|
| export type DirectConnectCallbacks = { |
| onMessage: (message: SDKMessage) => void |
| onPermissionRequest: ( |
| request: SDKControlPermissionRequest, |
| requestId: string, |
| ) => void |
| onConnected?: () => void |
| onDisconnected?: () => void |
| onError?: (error: Error) => void |
| } |
|
|
| function isStdoutMessage(value: unknown): value is StdoutMessage { |
| return ( |
| typeof value === 'object' && |
| value !== null && |
| 'type' in value && |
| typeof value.type === 'string' |
| ) |
| } |
|
|
| export class DirectConnectSessionManager { |
| private ws: WebSocket | null = null |
| private config: DirectConnectConfig |
| private callbacks: DirectConnectCallbacks |
|
|
| constructor(config: DirectConnectConfig, callbacks: DirectConnectCallbacks) { |
| this.config = config |
| this.callbacks = callbacks |
| } |
|
|
| connect(): void { |
| const headers: Record<string, string> = {} |
| if (this.config.authToken) { |
| headers['authorization'] = `Bearer ${this.config.authToken}` |
| } |
| |
| this.ws = new WebSocket(this.config.wsUrl, { |
| headers, |
| } as unknown as string[]) |
|
|
| this.ws.addEventListener('open', () => { |
| this.callbacks.onConnected?.() |
| }) |
|
|
| this.ws.addEventListener('message', event => { |
| const data = typeof event.data === 'string' ? event.data : '' |
| const lines = data.split('\n').filter((l: string) => l.trim()) |
|
|
| for (const line of lines) { |
| let raw: unknown |
| try { |
| raw = jsonParse(line) |
| } catch { |
| continue |
| } |
|
|
| if (!isStdoutMessage(raw)) { |
| continue |
| } |
| const parsed = raw |
|
|
| |
| if (parsed.type === 'control_request') { |
| if (parsed.request.subtype === 'can_use_tool') { |
| this.callbacks.onPermissionRequest( |
| parsed.request, |
| parsed.request_id, |
| ) |
| } else { |
| |
| |
| logForDebugging( |
| `[DirectConnect] Unsupported control request subtype: ${parsed.request.subtype}`, |
| ) |
| this.sendErrorResponse( |
| parsed.request_id, |
| `Unsupported control request subtype: ${parsed.request.subtype}`, |
| ) |
| } |
| continue |
| } |
|
|
| |
| if ( |
| parsed.type !== 'control_response' && |
| parsed.type !== 'keep_alive' && |
| parsed.type !== 'control_cancel_request' && |
| parsed.type !== 'streamlined_text' && |
| parsed.type !== 'streamlined_tool_use_summary' && |
| !(parsed.type === 'system' && parsed.subtype === 'post_turn_summary') |
| ) { |
| this.callbacks.onMessage(parsed) |
| } |
| } |
| }) |
|
|
| this.ws.addEventListener('close', () => { |
| this.callbacks.onDisconnected?.() |
| }) |
|
|
| this.ws.addEventListener('error', () => { |
| this.callbacks.onError?.(new Error('WebSocket connection error')) |
| }) |
| } |
|
|
| sendMessage(content: RemoteMessageContent): boolean { |
| if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { |
| return false |
| } |
|
|
| |
| const message = jsonStringify({ |
| type: 'user', |
| message: { |
| role: 'user', |
| content: content, |
| }, |
| parent_tool_use_id: null, |
| session_id: '', |
| }) |
| this.ws.send(message) |
| return true |
| } |
|
|
| respondToPermissionRequest( |
| requestId: string, |
| result: RemotePermissionResponse, |
| ): void { |
| if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { |
| return |
| } |
|
|
| |
| const response = jsonStringify({ |
| type: 'control_response', |
| response: { |
| subtype: 'success', |
| request_id: requestId, |
| response: { |
| behavior: result.behavior, |
| ...(result.behavior === 'allow' |
| ? { updatedInput: result.updatedInput } |
| : { message: result.message }), |
| }, |
| }, |
| }) |
| this.ws.send(response) |
| } |
|
|
| |
| |
| |
| sendInterrupt(): void { |
| if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { |
| return |
| } |
|
|
| |
| const request = jsonStringify({ |
| type: 'control_request', |
| request_id: crypto.randomUUID(), |
| request: { |
| subtype: 'interrupt', |
| }, |
| }) |
| this.ws.send(request) |
| } |
|
|
| private sendErrorResponse(requestId: string, error: string): void { |
| if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { |
| return |
| } |
| const response = jsonStringify({ |
| type: 'control_response', |
| response: { |
| subtype: 'error', |
| request_id: requestId, |
| error, |
| }, |
| }) |
| this.ws.send(response) |
| } |
|
|
| disconnect(): void { |
| if (this.ws) { |
| this.ws.close() |
| this.ws = null |
| } |
| } |
|
|
| isConnected(): boolean { |
| return this.ws?.readyState === WebSocket.OPEN |
| } |
| } |
|
|