Class: ClaudeAgentSDK::MessageParser

Inherits:
Object
  • Object
show all
Defined in:
lib/claude_agent_sdk/message_parser.rb

Overview

Parse message from CLI output into typed Message objects

Constant Summary collapse

SYSTEM_MESSAGE_CLASSES =

Typed SystemMessage subclasses inherit from Type and accept the raw CLI hash directly — camelCase and snake_case keys are normalized by the base class, and the full hash is captured as #data.

{
  'init' => InitMessage,
  'compact_boundary' => CompactBoundaryMessage,
  'status' => StatusMessage,
  'api_retry' => APIRetryMessage,
  'local_command_output' => LocalCommandOutputMessage,
  'mirror_error' => MirrorErrorMessage,
  'hook_started' => HookStartedMessage,
  'hook_progress' => HookProgressMessage,
  'hook_response' => HookResponseMessage,
  'session_state_changed' => SessionStateChangedMessage,
  'files_persisted' => FilesPersistedMessage,
  'elicitation_complete' => ElicitationCompleteMessage,
  'task_started' => TaskStartedMessage,
  'task_progress' => TaskProgressMessage,
  'task_notification' => TaskNotificationMessage,
  # task_updated carries `status` inside `patch` (not at the top level) and
  # defaults task_id to "" — it derives those defensively in its own
  # constructor (see TaskUpdatedMessage), so it dispatches through the table
  # like every other system subtype. `data` is always symbol-keyed here:
  # `parse` rejects any message lacking a `:type` symbol key, so a
  # string-keyed hash never reaches these classes.
  'task_updated' => TaskUpdatedMessage
}.freeze

Class Method Summary collapse

Class Method Details

.parse(data) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/claude_agent_sdk/message_parser.rb', line 9

def self.parse(data)
  raise MessageParseError.new("Invalid message data type", data: data) unless data.is_a?(Hash)

  message_type = data[:type]
  raise MessageParseError.new("Message missing 'type' field", data: data) unless message_type

  case message_type
  when 'user'
    parse_user_message(data)
  when 'assistant'
    parse_assistant_message(data)
  when 'system'
    parse_system_message(data)
  when 'result'
    parse_result_message(data)
  when 'stream_event'
    parse_stream_event(data)
  when 'rate_limit_event'
    parse_rate_limit_event(data)
  when 'conversation_reset'
    parse_conversation_reset_message(data)
  when 'tool_progress'
    parse_tool_progress_message(data)
  when 'auth_status'
    parse_auth_status_message(data)
  when 'tool_use_summary'
    parse_tool_use_summary_message(data)
  when 'prompt_suggestion'
    parse_prompt_suggestion_message(data)
  end
  # Forward-compatible: returns nil for unrecognized message types so
  # newer CLI versions don't crash older SDK versions.
rescue KeyError => e
  raise MessageParseError.new("Missing required field: #{e.message}", data: data)
end

.parse_assistant_message(data) ⇒ Object

Raises:



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/claude_agent_sdk/message_parser.rb', line 84

def self.parse_assistant_message(data)
  message_data = data[:message]
  # A non-Hash message (malformed CLI output) raised a raw TypeError from
  # dig instead of the documented MessageParseError.
  raise MessageParseError.new("Invalid message field in assistant message (expected Hash, got #{message_data.class})", data: data) unless message_data.is_a?(Hash)

  content = message_data[:content]
  raise MessageParseError.new("Missing content in assistant message", data: data) unless content
  raise MessageParseError.new("Invalid assistant content (expected Array, got #{content.class})", data: data) unless content.is_a?(Array)

  content_blocks = parse_content_blocks(content, data)
  AssistantMessage.new(
    content: content_blocks,
    # model is required, like Python — fetch raises KeyError when absent,
    # which parse's rescue wraps into MessageParseError, instead of
    # silently constructing model: nil.
    model: message_data.fetch(:model),
    parent_tool_use_id: data[:parent_tool_use_id],
    error: data[:error], # authentication_failed, billing_error, rate_limit, invalid_request, server_error, unknown
    usage: message_data[:usage],
    message_id: message_data[:id],
    stop_reason: message_data[:stop_reason],
    session_id: data[:session_id],
    uuid: data[:uuid]
  )
end

.parse_auth_status_message(data) ⇒ Object



177
178
179
# File 'lib/claude_agent_sdk/message_parser.rb', line 177

def self.parse_auth_status_message(data)
  AuthStatusMessage.new(data)
end

.parse_content_block(block) ⇒ Object

Accepts blocks with either symbol or string keys — live CLI messages arrive symbol-keyed (parsed via symbolize_names: true), session transcripts arrive string-keyed (parsed via symbolize_names: false). Uses a nil-aware fallback so is_error: false survives.



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/claude_agent_sdk/message_parser.rb', line 205

def self.parse_content_block(block)
  get = lambda do |key|
    v = block[key]
    v.nil? ? block[key.to_s] : v
  end
  case get.call(:type)
  when 'text'
    TextBlock.new(text: get.call(:text))
  when 'thinking'
    ThinkingBlock.new(thinking: get.call(:thinking), signature: get.call(:signature))
  when 'tool_use'
    ToolUseBlock.new(id: get.call(:id), name: get.call(:name), input: get.call(:input))
  when 'tool_result'
    ToolResultBlock.new(
      tool_use_id: get.call(:tool_use_id),
      content: get.call(:content),
      is_error: get.call(:is_error)
    )
  when 'server_tool_use'
    ServerToolUseBlock.new(id: get.call(:id), name: get.call(:name), input: get.call(:input))
  when 'advisor_tool_result'
    # The CLI's wire type for server-side tool results is
    # advisor_tool_result (the old 'server_tool_result' branch was dead
    # code — no CLI version emits it; Python parses advisor_tool_result).
    ServerToolResultBlock.new(
      tool_use_id: get.call(:tool_use_id),
      content: get.call(:content),
      is_error: get.call(:is_error)
    )
  else
    # Forward-compatible: preserve unrecognized content block types (e.g., "document", "image")
    # so newer CLI versions don't crash older SDK versions.
    UnknownBlock.new(type: get.call(:type), data: block)
  end
end

.parse_content_blocks(content, data) ⇒ Object

Maps a content Array to typed blocks, guarding each element. A non-Hash block (e.g. a bare String or nil from a malformed CLI message) raises a descriptive MessageParseError carrying the full message rather than an opaque TypeError/NoMethodError from block[:type] deep in parsing.



193
194
195
196
197
198
199
# File 'lib/claude_agent_sdk/message_parser.rb', line 193

def self.parse_content_blocks(content, data)
  content.map do |block|
    raise MessageParseError.new("Invalid content block (expected Hash, got #{block.class})", data: data) unless block.is_a?(Hash)

    parse_content_block(block)
  end
end

.parse_conversation_reset_message(data) ⇒ Object

/clear (or any other mid-session transcript discard) resets the conversation without ending the connection. Every field is required — a frame missing one is malformed CLI output, not a forward-compatible variant, so it raises rather than yielding a half-built message.



163
164
165
166
167
168
169
170
171
# File 'lib/claude_agent_sdk/message_parser.rb', line 163

def self.parse_conversation_reset_message(data)
  ConversationResetMessage.new(
    new_conversation_id: data.fetch(:new_conversation_id),
    uuid: data.fetch(:uuid),
    session_id: data.fetch(:session_id)
  )
rescue KeyError => e
  raise MessageParseError.new("Missing required field in conversation_reset message: #{e.key}", data: data)
end

.parse_prompt_suggestion_message(data) ⇒ Object



185
186
187
# File 'lib/claude_agent_sdk/message_parser.rb', line 185

def self.parse_prompt_suggestion_message(data)
  PromptSuggestionMessage.new(data)
end

.parse_rate_limit_event(data) ⇒ Object



155
156
157
# File 'lib/claude_agent_sdk/message_parser.rb', line 155

def self.parse_rate_limit_event(data)
  RateLimitEvent.new(data.merge(raw_data: data))
end

.parse_result_message(data) ⇒ Object



144
145
146
147
148
149
# File 'lib/claude_agent_sdk/message_parser.rb', line 144

def self.parse_result_message(data)
  # `origin` is overwritten with the validated value (or nil) rather than
  # letting the base class assign the raw field: a malformed origin must
  # read as absent, not be surfaced verbatim.
  ResultMessage.new(data.merge(origin: parse_origin(data)))
end

.parse_stream_event(data) ⇒ Object



151
152
153
# File 'lib/claude_agent_sdk/message_parser.rb', line 151

def self.parse_stream_event(data)
  StreamEvent.new(data)
end

.parse_system_message(data) ⇒ Object



139
140
141
142
# File 'lib/claude_agent_sdk/message_parser.rb', line 139

def self.parse_system_message(data)
  klass = SYSTEM_MESSAGE_CLASSES[data[:subtype]] || SystemMessage
  klass.new(data)
end

.parse_tool_progress_message(data) ⇒ Object



173
174
175
# File 'lib/claude_agent_sdk/message_parser.rb', line 173

def self.parse_tool_progress_message(data)
  ToolProgressMessage.new(data)
end

.parse_tool_use_summary_message(data) ⇒ Object



181
182
183
# File 'lib/claude_agent_sdk/message_parser.rb', line 181

def self.parse_tool_use_summary_message(data)
  ToolUseSummaryMessage.new(data)
end

.parse_user_message(data) ⇒ Object

Raises:



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/claude_agent_sdk/message_parser.rb', line 45

def self.parse_user_message(data)
  parent_tool_use_id = data[:parent_tool_use_id]
  uuid = data[:uuid] # UUID for rewind support
  tool_use_result = data[:tool_use_result]
  message_data = data[:message]
  raise MessageParseError.new("Missing message field in user message", data: data) unless message_data
  # A non-Hash message (malformed CLI output) raised a raw TypeError from
  # message_data[:content] instead of the documented MessageParseError.
  raise MessageParseError.new("Invalid message field in user message (expected Hash, got #{message_data.class})", data: data) unless message_data.is_a?(Hash)

  content = message_data[:content]
  raise MessageParseError.new("Missing content in user message", data: data) unless content

  origin = parse_origin(data)

  if content.is_a?(Array)
    content_blocks = parse_content_blocks(content, data)
    UserMessage.new(content: content_blocks, uuid: uuid, parent_tool_use_id: parent_tool_use_id,
                    tool_use_result: tool_use_result, origin: origin)
  else
    UserMessage.new(content: content, uuid: uuid, parent_tool_use_id: parent_tool_use_id,
                    tool_use_result: tool_use_result, origin: origin)
  end
end