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:



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/claude_agent_sdk/message_parser.rb', line 63

def self.parse_assistant_message(data)
  content = data.dig(:message, :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: data.dig(:message, :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: data.dig(:message, :usage),
    message_id: data.dig(:message, :id),
    stop_reason: data.dig(:message, :stop_reason),
    session_id: data[:session_id],
    uuid: data[:uuid]
  )
end

.parse_auth_status_message(data) ⇒ Object



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

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.



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/claude_agent_sdk/message_parser.rb', line 159

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.



147
148
149
150
151
152
153
# File 'lib/claude_agent_sdk/message_parser.rb', line 147

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



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

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

.parse_rate_limit_event(data) ⇒ Object



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

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

.parse_result_message(data) ⇒ Object



115
116
117
# File 'lib/claude_agent_sdk/message_parser.rb', line 115

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

.parse_stream_event(data) ⇒ Object



119
120
121
# File 'lib/claude_agent_sdk/message_parser.rb', line 119

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

.parse_system_message(data) ⇒ Object



110
111
112
113
# File 'lib/claude_agent_sdk/message_parser.rb', line 110

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

.parse_tool_progress_message(data) ⇒ Object



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

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

.parse_tool_use_summary_message(data) ⇒ Object



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

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

  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