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
# 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 '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:



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/claude_agent_sdk/message_parser.rb', line 66

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



142
143
144
# File 'lib/claude_agent_sdk/message_parser.rb', line 142

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.



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/claude_agent_sdk/message_parser.rb', line 170

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.



158
159
160
161
162
163
164
# File 'lib/claude_agent_sdk/message_parser.rb', line 158

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_prompt_suggestion_message(data) ⇒ Object



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

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

.parse_rate_limit_event(data) ⇒ Object



134
135
136
# File 'lib/claude_agent_sdk/message_parser.rb', line 134

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

.parse_result_message(data) ⇒ Object



126
127
128
# File 'lib/claude_agent_sdk/message_parser.rb', line 126

def self.parse_result_message(data)
  ResultMessage.new(data)
end

.parse_stream_event(data) ⇒ Object



130
131
132
# File 'lib/claude_agent_sdk/message_parser.rb', line 130

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

.parse_system_message(data) ⇒ Object



121
122
123
124
# File 'lib/claude_agent_sdk/message_parser.rb', line 121

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

.parse_tool_progress_message(data) ⇒ Object



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

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

.parse_tool_use_summary_message(data) ⇒ Object



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

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

.parse_user_message(data) ⇒ Object

Raises:



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

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

  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)
  else
    UserMessage.new(content: content, uuid: uuid, parent_tool_use_id: parent_tool_use_id,
                    tool_use_result: tool_use_result)
  end
end