chore: initialize recovered claude workspace
This commit is contained in:
302
src/remote/sdkMessageAdapter.ts
Normal file
302
src/remote/sdkMessageAdapter.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import type {
|
||||
SDKAssistantMessage,
|
||||
SDKCompactBoundaryMessage,
|
||||
SDKMessage,
|
||||
SDKPartialAssistantMessage,
|
||||
SDKResultMessage,
|
||||
SDKStatusMessage,
|
||||
SDKSystemMessage,
|
||||
SDKToolProgressMessage,
|
||||
} from '../entrypoints/agentSdkTypes.js'
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Message,
|
||||
StreamEvent,
|
||||
SystemMessage,
|
||||
} from '../types/message.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { fromSDKCompactMetadata } from '../utils/messages/mappers.js'
|
||||
import { createUserMessage } from '../utils/messages.js'
|
||||
|
||||
/**
|
||||
* Converts SDKMessage from CCR to REPL Message types.
|
||||
*
|
||||
* The CCR backend sends SDK-format messages via WebSocket. The REPL expects
|
||||
* internal Message types for rendering. This adapter bridges the two.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert an SDKAssistantMessage to an AssistantMessage
|
||||
*/
|
||||
function convertAssistantMessage(msg: SDKAssistantMessage): AssistantMessage {
|
||||
return {
|
||||
type: 'assistant',
|
||||
message: msg.message,
|
||||
uuid: msg.uuid,
|
||||
requestId: undefined,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: msg.error,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an SDKPartialAssistantMessage (streaming) to a StreamEvent
|
||||
*/
|
||||
function convertStreamEvent(msg: SDKPartialAssistantMessage): StreamEvent {
|
||||
return {
|
||||
type: 'stream_event',
|
||||
event: msg.event,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an SDKResultMessage to a SystemMessage
|
||||
*/
|
||||
function convertResultMessage(msg: SDKResultMessage): SystemMessage {
|
||||
const isError = msg.subtype !== 'success'
|
||||
const content = isError
|
||||
? msg.errors?.join(', ') || 'Unknown error'
|
||||
: 'Session completed successfully'
|
||||
|
||||
return {
|
||||
type: 'system',
|
||||
subtype: 'informational',
|
||||
content,
|
||||
level: isError ? 'warning' : 'info',
|
||||
uuid: msg.uuid,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an SDKSystemMessage (init) to a SystemMessage
|
||||
*/
|
||||
function convertInitMessage(msg: SDKSystemMessage): SystemMessage {
|
||||
return {
|
||||
type: 'system',
|
||||
subtype: 'informational',
|
||||
content: `Remote session initialized (model: ${msg.model})`,
|
||||
level: 'info',
|
||||
uuid: msg.uuid,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an SDKStatusMessage to a SystemMessage
|
||||
*/
|
||||
function convertStatusMessage(msg: SDKStatusMessage): SystemMessage | null {
|
||||
if (!msg.status) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'system',
|
||||
subtype: 'informational',
|
||||
content:
|
||||
msg.status === 'compacting'
|
||||
? 'Compacting conversation…'
|
||||
: `Status: ${msg.status}`,
|
||||
level: 'info',
|
||||
uuid: msg.uuid,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an SDKToolProgressMessage to a SystemMessage.
|
||||
* We use a system message instead of ProgressMessage since the Progress type
|
||||
* is a complex union that requires tool-specific data we don't have from CCR.
|
||||
*/
|
||||
function convertToolProgressMessage(
|
||||
msg: SDKToolProgressMessage,
|
||||
): SystemMessage {
|
||||
return {
|
||||
type: 'system',
|
||||
subtype: 'informational',
|
||||
content: `Tool ${msg.tool_name} running for ${msg.elapsed_time_seconds}s…`,
|
||||
level: 'info',
|
||||
uuid: msg.uuid,
|
||||
timestamp: new Date().toISOString(),
|
||||
toolUseID: msg.tool_use_id,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an SDKCompactBoundaryMessage to a SystemMessage
|
||||
*/
|
||||
function convertCompactBoundaryMessage(
|
||||
msg: SDKCompactBoundaryMessage,
|
||||
): SystemMessage {
|
||||
return {
|
||||
type: 'system',
|
||||
subtype: 'compact_boundary',
|
||||
content: 'Conversation compacted',
|
||||
level: 'info',
|
||||
uuid: msg.uuid,
|
||||
timestamp: new Date().toISOString(),
|
||||
compactMetadata: fromSDKCompactMetadata(msg.compact_metadata),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of converting an SDKMessage
|
||||
*/
|
||||
export type ConvertedMessage =
|
||||
| { type: 'message'; message: Message }
|
||||
| { type: 'stream_event'; event: StreamEvent }
|
||||
| { type: 'ignored' }
|
||||
|
||||
type ConvertOptions = {
|
||||
/** Convert user messages containing tool_result content blocks into UserMessages.
|
||||
* Used by direct connect mode where tool results come from the remote server
|
||||
* and need to be rendered locally. CCR mode ignores user messages since they
|
||||
* are handled differently. */
|
||||
convertToolResults?: boolean
|
||||
/**
|
||||
* Convert user text messages into UserMessages for display. Used when
|
||||
* converting historical events where user-typed messages need to be shown.
|
||||
* In live WS mode these are already added locally by the REPL so they're
|
||||
* ignored by default.
|
||||
*/
|
||||
convertUserTextMessages?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an SDKMessage to REPL message format
|
||||
*/
|
||||
export function convertSDKMessage(
|
||||
msg: SDKMessage,
|
||||
opts?: ConvertOptions,
|
||||
): ConvertedMessage {
|
||||
switch (msg.type) {
|
||||
case 'assistant':
|
||||
return { type: 'message', message: convertAssistantMessage(msg) }
|
||||
|
||||
case 'user': {
|
||||
const content = msg.message?.content
|
||||
// Tool result messages from the remote server need to be converted so
|
||||
// they render and collapse like local tool results. Detect via content
|
||||
// shape (tool_result blocks) — parent_tool_use_id is NOT reliable: the
|
||||
// agent-side normalizeMessage() hardcodes it to null for top-level
|
||||
// tool results, so it can't distinguish tool results from prompt echoes.
|
||||
const isToolResult =
|
||||
Array.isArray(content) && content.some(b => b.type === 'tool_result')
|
||||
if (opts?.convertToolResults && isToolResult) {
|
||||
return {
|
||||
type: 'message',
|
||||
message: createUserMessage({
|
||||
content,
|
||||
toolUseResult: msg.tool_use_result,
|
||||
uuid: msg.uuid,
|
||||
timestamp: msg.timestamp,
|
||||
}),
|
||||
}
|
||||
}
|
||||
// When converting historical events, user-typed messages need to be
|
||||
// rendered (they weren't added locally by the REPL). Skip tool_results
|
||||
// here — already handled above.
|
||||
if (opts?.convertUserTextMessages && !isToolResult) {
|
||||
if (typeof content === 'string' || Array.isArray(content)) {
|
||||
return {
|
||||
type: 'message',
|
||||
message: createUserMessage({
|
||||
content,
|
||||
toolUseResult: msg.tool_use_result,
|
||||
uuid: msg.uuid,
|
||||
timestamp: msg.timestamp,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
// User-typed messages (string content) are already added locally by REPL.
|
||||
// In CCR mode, all user messages are ignored (tool results handled differently).
|
||||
return { type: 'ignored' }
|
||||
}
|
||||
|
||||
case 'stream_event':
|
||||
return { type: 'stream_event', event: convertStreamEvent(msg) }
|
||||
|
||||
case 'result':
|
||||
// Only show result messages for errors. Success results are noise
|
||||
// in multi-turn sessions (isLoading=false is sufficient signal).
|
||||
if (msg.subtype !== 'success') {
|
||||
return { type: 'message', message: convertResultMessage(msg) }
|
||||
}
|
||||
return { type: 'ignored' }
|
||||
|
||||
case 'system':
|
||||
if (msg.subtype === 'init') {
|
||||
return { type: 'message', message: convertInitMessage(msg) }
|
||||
}
|
||||
if (msg.subtype === 'status') {
|
||||
const statusMsg = convertStatusMessage(msg)
|
||||
return statusMsg
|
||||
? { type: 'message', message: statusMsg }
|
||||
: { type: 'ignored' }
|
||||
}
|
||||
if (msg.subtype === 'compact_boundary') {
|
||||
return {
|
||||
type: 'message',
|
||||
message: convertCompactBoundaryMessage(msg),
|
||||
}
|
||||
}
|
||||
// hook_response and other subtypes
|
||||
logForDebugging(
|
||||
`[sdkMessageAdapter] Ignoring system message subtype: ${msg.subtype}`,
|
||||
)
|
||||
return { type: 'ignored' }
|
||||
|
||||
case 'tool_progress':
|
||||
return { type: 'message', message: convertToolProgressMessage(msg) }
|
||||
|
||||
case 'auth_status':
|
||||
// Auth status is handled separately, not converted to a display message
|
||||
logForDebugging('[sdkMessageAdapter] Ignoring auth_status message')
|
||||
return { type: 'ignored' }
|
||||
|
||||
case 'tool_use_summary':
|
||||
// Tool use summaries are SDK-only events, not displayed in REPL
|
||||
logForDebugging('[sdkMessageAdapter] Ignoring tool_use_summary message')
|
||||
return { type: 'ignored' }
|
||||
|
||||
case 'rate_limit_event':
|
||||
// Rate limit events are SDK-only events, not displayed in REPL
|
||||
logForDebugging('[sdkMessageAdapter] Ignoring rate_limit_event message')
|
||||
return { type: 'ignored' }
|
||||
|
||||
default: {
|
||||
// Gracefully ignore unknown message types. The backend may send new
|
||||
// types before the client is updated; logging helps with debugging
|
||||
// without crashing or losing the session.
|
||||
logForDebugging(
|
||||
`[sdkMessageAdapter] Unknown message type: ${(msg as { type: string }).type}`,
|
||||
)
|
||||
return { type: 'ignored' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an SDKMessage indicates the session has ended
|
||||
*/
|
||||
export function isSessionEndMessage(msg: SDKMessage): boolean {
|
||||
return msg.type === 'result'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an SDKResultMessage indicates success
|
||||
*/
|
||||
export function isSuccessResult(msg: SDKResultMessage): boolean {
|
||||
return msg.subtype === 'success'
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the result text from a successful SDKResultMessage
|
||||
*/
|
||||
export function getResultText(msg: SDKResultMessage): string | null {
|
||||
if (msg.subtype === 'success') {
|
||||
return msg.result
|
||||
}
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user