Class: Legate::Event

Inherits:
Struct
  • Object
show all
Defined in:
lib/legate/event.rb

Overview

Represents a single interaction or step within a Session’s history. Immutable object after creation.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(role:, content:, timestamp: nil, tool_name: nil, state_delta: nil, event_id: nil) ⇒ Event

Returns a new instance of Event.

Parameters:

  • role (Symbol)

    :user, :agent, :tool_request, :tool_result

  • content (String, Hash)

    Event payload. Should be JSON-serializable.

  • timestamp (Time, nil) (defaults to: nil)

    Timestamp (defaults to Time.now.utc).

  • tool_name (Symbol, nil) (defaults to: nil)

    Name of the tool if role is tool related.

  • state_delta (Hash, nil) (defaults to: nil)

    State changes to apply with this event.

  • event_id (String, nil) (defaults to: nil)

    Unique event ID (defaults to SecureRandom.uuid).

Raises:

  • (ArgumentError)


31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/legate/event.rb', line 31

def initialize(role:, content:, timestamp: nil, tool_name: nil, state_delta: nil, event_id: nil)
  # Basic validation
  raise ArgumentError, "Invalid role: #{role}. Must be :user, :agent, :tool_request, or :tool_result." unless %i[user agent tool_request tool_result].include?(role)

  Legate.logger.warn("Event: :#{role} event created without a valid :tool_name symbol.") if %i[tool_request tool_result].include?(role) && (tool_name.nil? || !tool_name.is_a?(Symbol))

  # Validate state_delta is a Hash or nil
  unless state_delta.nil? || state_delta.is_a?(Hash)
    Legate.logger.warn("Event: :state_delta must be a Hash or nil, received #{state_delta.class}.")
    state_delta = nil # Force to nil if invalid
  end

  # Ensure content is somewhat reasonable (avoids deep inspection for performance)
  Legate.logger.warn("Event: Content is of unusual type (#{content.class}): #{content.inspect}") unless content.is_a?(String) || content.is_a?(Hash) || content.is_a?(Array) || content.is_a?(NilClass) || content.is_a?(Numeric) || content.is_a?(TrueClass) || content.is_a?(FalseClass)

  super(
    role: role,
    content: deep_freeze(content),
    timestamp: timestamp || Time.now.utc,
    tool_name: tool_name,
    state_delta: deep_freeze(state_delta&.transform_keys(&:to_sym)),
    event_id: event_id || SecureRandom.uuid
  )
  freeze
end

Instance Attribute Details

#contentString, Hash (readonly)

Returns The payload of the event (e.g., user text, agent text, tool params, tool result hash).

Returns:

  • (String, Hash)

    The payload of the event (e.g., user text, agent text, tool params, tool result hash).



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/legate/event.rb', line 24

Event = Struct.new(:role, :content, :timestamp, :tool_name, :state_delta, :event_id, keyword_init: true) do
  # @param role [Symbol] :user, :agent, :tool_request, :tool_result
  # @param content [String, Hash] Event payload. Should be JSON-serializable.
  # @param timestamp [Time, nil] Timestamp (defaults to Time.now.utc).
  # @param tool_name [Symbol, nil] Name of the tool if role is tool related.
  # @param state_delta [Hash, nil] State changes to apply with this event.
  # @param event_id [String, nil] Unique event ID (defaults to SecureRandom.uuid).
  def initialize(role:, content:, timestamp: nil, tool_name: nil, state_delta: nil, event_id: nil)
    # Basic validation
    raise ArgumentError, "Invalid role: #{role}. Must be :user, :agent, :tool_request, or :tool_result." unless %i[user agent tool_request tool_result].include?(role)

    Legate.logger.warn("Event: :#{role} event created without a valid :tool_name symbol.") if %i[tool_request tool_result].include?(role) && (tool_name.nil? || !tool_name.is_a?(Symbol))

    # Validate state_delta is a Hash or nil
    unless state_delta.nil? || state_delta.is_a?(Hash)
      Legate.logger.warn("Event: :state_delta must be a Hash or nil, received #{state_delta.class}.")
      state_delta = nil # Force to nil if invalid
    end

    # Ensure content is somewhat reasonable (avoids deep inspection for performance)
    Legate.logger.warn("Event: Content is of unusual type (#{content.class}): #{content.inspect}") unless content.is_a?(String) || content.is_a?(Hash) || content.is_a?(Array) || content.is_a?(NilClass) || content.is_a?(Numeric) || content.is_a?(TrueClass) || content.is_a?(FalseClass)

    super(
      role: role,
      content: deep_freeze(content),
      timestamp: timestamp || Time.now.utc,
      tool_name: tool_name,
      state_delta: deep_freeze(state_delta&.transform_keys(&:to_sym)),
      event_id: event_id || SecureRandom.uuid
    )
    freeze
  end

  private

  def deep_freeze(obj)
    case obj
    when Hash
      obj.each_value { |v| deep_freeze(v) }
      obj.freeze
    when Array
      obj.each { |v| deep_freeze(v) }
      obj.freeze
    when String
      obj.freeze
    else
      obj
    end
  end

  public

  # Helper to check if the event represents a final agent response to the user.
  # @return [Boolean]
  def final_agent_response?
    role == :agent
  end

  # --- Result accessors ---
  # Convenience readers over the standard { status:, result: / error_message: }
  # content hash, so callers don't reach into it. Meaningful on a final agent
  # event (e.g. the return of Agent#ask / #run_task); harmless elsewhere.

  # @return [Boolean] true if this carries a successful result
  def success?
    content.is_a?(Hash) && content[:status] == :success
  end

  # @return [Boolean] true if this carries an error result
  def error?
    content.is_a?(Hash) && content[:status] == :error
  end

  # The successful result value (nil on error). Non-Hash content is returned
  # as-is (e.g. a scalar result stored directly).
  # @return [Object, nil]
  def answer
    content.is_a?(Hash) ? content[:result] : content
  end

  # @return [String, nil] the error message, or nil when not an error
  def error_message
    content.is_a?(Hash) ? content[:error_message] : nil
  end

  # Basic serialization for storage (e.g., in Redis).
  # @return [Hash] A hash representation suitable for JSON conversion.
  def to_h
    {
      role: role,
      content: content, # Assumes content is already JSON-serializable
      timestamp: timestamp.iso8601(3), # Use ISO8601 format with milliseconds
      tool_name: tool_name,
      state_delta: state_delta, # Store the hash directly (must be JSON-serializable)
      event_id: event_id
    }
  end

  # Basic deserialization from a hash (e.g., after reading from JSON).
  # @param hash [Hash] The hash containing event data (uses symbolized keys).
  # @return [Legate::Event] A new Event object.
  def self.from_h(hash)
    # Optimized: Extract fields manually to avoid full hash allocation via transform_keys
    role = hash.key?(:role) ? hash[:role] : hash['role']
    content = hash.key?(:content) ? hash[:content] : hash['content']
    ts_val = hash.key?(:timestamp) ? hash[:timestamp] : hash['timestamp']
    tool_name = hash.key?(:tool_name) ? hash[:tool_name] : hash['tool_name']
    state_delta = hash.key?(:state_delta) ? hash[:state_delta] : hash['state_delta']

    # Validate state_delta type to preserve strict behavior (fail on invalid type)
    if state_delta && !state_delta.is_a?(Hash)
      Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): state_delta must be a Hash. Hash: #{hash.inspect}")
      return nil
    end

    event_id = hash.key?(:event_id) ? hash[:event_id] : hash['event_id']

    new(
      role: role&.to_sym,
      content: content,
      # Safely parse timestamp
      timestamp: ts_val ? Time.iso8601(ts_val) : Time.now.utc,
      tool_name: tool_name&.to_sym,
      # Pass state_delta directly; initialize handles validation and symbolization/copy
      state_delta: state_delta,
      event_id: event_id
    )
  rescue ArgumentError => e
    Legate.logger.error("Event.from_h: Failed to parse timestamp or invalid role: #{e.message}. Hash: #{hash.inspect}")
    # Decide on fallback: return nil, raise, or return partial object?
    # Returning nil might be safest to signal deserialization failure.
    nil
  rescue TypeError, NoMethodError => e # Also rescue NoMethodError
    Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): #{e.message}. Hash: #{hash.inspect}")
    nil
  end
end

#event_idString (readonly)

Returns A unique ID for this specific event instance.

Returns:

  • (String)

    A unique ID for this specific event instance.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/legate/event.rb', line 24

Event = Struct.new(:role, :content, :timestamp, :tool_name, :state_delta, :event_id, keyword_init: true) do
  # @param role [Symbol] :user, :agent, :tool_request, :tool_result
  # @param content [String, Hash] Event payload. Should be JSON-serializable.
  # @param timestamp [Time, nil] Timestamp (defaults to Time.now.utc).
  # @param tool_name [Symbol, nil] Name of the tool if role is tool related.
  # @param state_delta [Hash, nil] State changes to apply with this event.
  # @param event_id [String, nil] Unique event ID (defaults to SecureRandom.uuid).
  def initialize(role:, content:, timestamp: nil, tool_name: nil, state_delta: nil, event_id: nil)
    # Basic validation
    raise ArgumentError, "Invalid role: #{role}. Must be :user, :agent, :tool_request, or :tool_result." unless %i[user agent tool_request tool_result].include?(role)

    Legate.logger.warn("Event: :#{role} event created without a valid :tool_name symbol.") if %i[tool_request tool_result].include?(role) && (tool_name.nil? || !tool_name.is_a?(Symbol))

    # Validate state_delta is a Hash or nil
    unless state_delta.nil? || state_delta.is_a?(Hash)
      Legate.logger.warn("Event: :state_delta must be a Hash or nil, received #{state_delta.class}.")
      state_delta = nil # Force to nil if invalid
    end

    # Ensure content is somewhat reasonable (avoids deep inspection for performance)
    Legate.logger.warn("Event: Content is of unusual type (#{content.class}): #{content.inspect}") unless content.is_a?(String) || content.is_a?(Hash) || content.is_a?(Array) || content.is_a?(NilClass) || content.is_a?(Numeric) || content.is_a?(TrueClass) || content.is_a?(FalseClass)

    super(
      role: role,
      content: deep_freeze(content),
      timestamp: timestamp || Time.now.utc,
      tool_name: tool_name,
      state_delta: deep_freeze(state_delta&.transform_keys(&:to_sym)),
      event_id: event_id || SecureRandom.uuid
    )
    freeze
  end

  private

  def deep_freeze(obj)
    case obj
    when Hash
      obj.each_value { |v| deep_freeze(v) }
      obj.freeze
    when Array
      obj.each { |v| deep_freeze(v) }
      obj.freeze
    when String
      obj.freeze
    else
      obj
    end
  end

  public

  # Helper to check if the event represents a final agent response to the user.
  # @return [Boolean]
  def final_agent_response?
    role == :agent
  end

  # --- Result accessors ---
  # Convenience readers over the standard { status:, result: / error_message: }
  # content hash, so callers don't reach into it. Meaningful on a final agent
  # event (e.g. the return of Agent#ask / #run_task); harmless elsewhere.

  # @return [Boolean] true if this carries a successful result
  def success?
    content.is_a?(Hash) && content[:status] == :success
  end

  # @return [Boolean] true if this carries an error result
  def error?
    content.is_a?(Hash) && content[:status] == :error
  end

  # The successful result value (nil on error). Non-Hash content is returned
  # as-is (e.g. a scalar result stored directly).
  # @return [Object, nil]
  def answer
    content.is_a?(Hash) ? content[:result] : content
  end

  # @return [String, nil] the error message, or nil when not an error
  def error_message
    content.is_a?(Hash) ? content[:error_message] : nil
  end

  # Basic serialization for storage (e.g., in Redis).
  # @return [Hash] A hash representation suitable for JSON conversion.
  def to_h
    {
      role: role,
      content: content, # Assumes content is already JSON-serializable
      timestamp: timestamp.iso8601(3), # Use ISO8601 format with milliseconds
      tool_name: tool_name,
      state_delta: state_delta, # Store the hash directly (must be JSON-serializable)
      event_id: event_id
    }
  end

  # Basic deserialization from a hash (e.g., after reading from JSON).
  # @param hash [Hash] The hash containing event data (uses symbolized keys).
  # @return [Legate::Event] A new Event object.
  def self.from_h(hash)
    # Optimized: Extract fields manually to avoid full hash allocation via transform_keys
    role = hash.key?(:role) ? hash[:role] : hash['role']
    content = hash.key?(:content) ? hash[:content] : hash['content']
    ts_val = hash.key?(:timestamp) ? hash[:timestamp] : hash['timestamp']
    tool_name = hash.key?(:tool_name) ? hash[:tool_name] : hash['tool_name']
    state_delta = hash.key?(:state_delta) ? hash[:state_delta] : hash['state_delta']

    # Validate state_delta type to preserve strict behavior (fail on invalid type)
    if state_delta && !state_delta.is_a?(Hash)
      Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): state_delta must be a Hash. Hash: #{hash.inspect}")
      return nil
    end

    event_id = hash.key?(:event_id) ? hash[:event_id] : hash['event_id']

    new(
      role: role&.to_sym,
      content: content,
      # Safely parse timestamp
      timestamp: ts_val ? Time.iso8601(ts_val) : Time.now.utc,
      tool_name: tool_name&.to_sym,
      # Pass state_delta directly; initialize handles validation and symbolization/copy
      state_delta: state_delta,
      event_id: event_id
    )
  rescue ArgumentError => e
    Legate.logger.error("Event.from_h: Failed to parse timestamp or invalid role: #{e.message}. Hash: #{hash.inspect}")
    # Decide on fallback: return nil, raise, or return partial object?
    # Returning nil might be safest to signal deserialization failure.
    nil
  rescue TypeError, NoMethodError => e # Also rescue NoMethodError
    Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): #{e.message}. Hash: #{hash.inspect}")
    nil
  end
end

#roleSymbol (readonly)

Returns The origin of the event (:user, :agent, :tool_request, :tool_result).

Returns:

  • (Symbol)

    The origin of the event (:user, :agent, :tool_request, :tool_result).



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/legate/event.rb', line 24

Event = Struct.new(:role, :content, :timestamp, :tool_name, :state_delta, :event_id, keyword_init: true) do
  # @param role [Symbol] :user, :agent, :tool_request, :tool_result
  # @param content [String, Hash] Event payload. Should be JSON-serializable.
  # @param timestamp [Time, nil] Timestamp (defaults to Time.now.utc).
  # @param tool_name [Symbol, nil] Name of the tool if role is tool related.
  # @param state_delta [Hash, nil] State changes to apply with this event.
  # @param event_id [String, nil] Unique event ID (defaults to SecureRandom.uuid).
  def initialize(role:, content:, timestamp: nil, tool_name: nil, state_delta: nil, event_id: nil)
    # Basic validation
    raise ArgumentError, "Invalid role: #{role}. Must be :user, :agent, :tool_request, or :tool_result." unless %i[user agent tool_request tool_result].include?(role)

    Legate.logger.warn("Event: :#{role} event created without a valid :tool_name symbol.") if %i[tool_request tool_result].include?(role) && (tool_name.nil? || !tool_name.is_a?(Symbol))

    # Validate state_delta is a Hash or nil
    unless state_delta.nil? || state_delta.is_a?(Hash)
      Legate.logger.warn("Event: :state_delta must be a Hash or nil, received #{state_delta.class}.")
      state_delta = nil # Force to nil if invalid
    end

    # Ensure content is somewhat reasonable (avoids deep inspection for performance)
    Legate.logger.warn("Event: Content is of unusual type (#{content.class}): #{content.inspect}") unless content.is_a?(String) || content.is_a?(Hash) || content.is_a?(Array) || content.is_a?(NilClass) || content.is_a?(Numeric) || content.is_a?(TrueClass) || content.is_a?(FalseClass)

    super(
      role: role,
      content: deep_freeze(content),
      timestamp: timestamp || Time.now.utc,
      tool_name: tool_name,
      state_delta: deep_freeze(state_delta&.transform_keys(&:to_sym)),
      event_id: event_id || SecureRandom.uuid
    )
    freeze
  end

  private

  def deep_freeze(obj)
    case obj
    when Hash
      obj.each_value { |v| deep_freeze(v) }
      obj.freeze
    when Array
      obj.each { |v| deep_freeze(v) }
      obj.freeze
    when String
      obj.freeze
    else
      obj
    end
  end

  public

  # Helper to check if the event represents a final agent response to the user.
  # @return [Boolean]
  def final_agent_response?
    role == :agent
  end

  # --- Result accessors ---
  # Convenience readers over the standard { status:, result: / error_message: }
  # content hash, so callers don't reach into it. Meaningful on a final agent
  # event (e.g. the return of Agent#ask / #run_task); harmless elsewhere.

  # @return [Boolean] true if this carries a successful result
  def success?
    content.is_a?(Hash) && content[:status] == :success
  end

  # @return [Boolean] true if this carries an error result
  def error?
    content.is_a?(Hash) && content[:status] == :error
  end

  # The successful result value (nil on error). Non-Hash content is returned
  # as-is (e.g. a scalar result stored directly).
  # @return [Object, nil]
  def answer
    content.is_a?(Hash) ? content[:result] : content
  end

  # @return [String, nil] the error message, or nil when not an error
  def error_message
    content.is_a?(Hash) ? content[:error_message] : nil
  end

  # Basic serialization for storage (e.g., in Redis).
  # @return [Hash] A hash representation suitable for JSON conversion.
  def to_h
    {
      role: role,
      content: content, # Assumes content is already JSON-serializable
      timestamp: timestamp.iso8601(3), # Use ISO8601 format with milliseconds
      tool_name: tool_name,
      state_delta: state_delta, # Store the hash directly (must be JSON-serializable)
      event_id: event_id
    }
  end

  # Basic deserialization from a hash (e.g., after reading from JSON).
  # @param hash [Hash] The hash containing event data (uses symbolized keys).
  # @return [Legate::Event] A new Event object.
  def self.from_h(hash)
    # Optimized: Extract fields manually to avoid full hash allocation via transform_keys
    role = hash.key?(:role) ? hash[:role] : hash['role']
    content = hash.key?(:content) ? hash[:content] : hash['content']
    ts_val = hash.key?(:timestamp) ? hash[:timestamp] : hash['timestamp']
    tool_name = hash.key?(:tool_name) ? hash[:tool_name] : hash['tool_name']
    state_delta = hash.key?(:state_delta) ? hash[:state_delta] : hash['state_delta']

    # Validate state_delta type to preserve strict behavior (fail on invalid type)
    if state_delta && !state_delta.is_a?(Hash)
      Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): state_delta must be a Hash. Hash: #{hash.inspect}")
      return nil
    end

    event_id = hash.key?(:event_id) ? hash[:event_id] : hash['event_id']

    new(
      role: role&.to_sym,
      content: content,
      # Safely parse timestamp
      timestamp: ts_val ? Time.iso8601(ts_val) : Time.now.utc,
      tool_name: tool_name&.to_sym,
      # Pass state_delta directly; initialize handles validation and symbolization/copy
      state_delta: state_delta,
      event_id: event_id
    )
  rescue ArgumentError => e
    Legate.logger.error("Event.from_h: Failed to parse timestamp or invalid role: #{e.message}. Hash: #{hash.inspect}")
    # Decide on fallback: return nil, raise, or return partial object?
    # Returning nil might be safest to signal deserialization failure.
    nil
  rescue TypeError, NoMethodError => e # Also rescue NoMethodError
    Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): #{e.message}. Hash: #{hash.inspect}")
    nil
  end
end

#state_deltaHash? (readonly)

Returns Optional hash representing state changes associated with this event. Keys should be symbols.

Returns:

  • (Hash, nil)

    Optional hash representing state changes associated with this event. Keys should be symbols.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/legate/event.rb', line 24

Event = Struct.new(:role, :content, :timestamp, :tool_name, :state_delta, :event_id, keyword_init: true) do
  # @param role [Symbol] :user, :agent, :tool_request, :tool_result
  # @param content [String, Hash] Event payload. Should be JSON-serializable.
  # @param timestamp [Time, nil] Timestamp (defaults to Time.now.utc).
  # @param tool_name [Symbol, nil] Name of the tool if role is tool related.
  # @param state_delta [Hash, nil] State changes to apply with this event.
  # @param event_id [String, nil] Unique event ID (defaults to SecureRandom.uuid).
  def initialize(role:, content:, timestamp: nil, tool_name: nil, state_delta: nil, event_id: nil)
    # Basic validation
    raise ArgumentError, "Invalid role: #{role}. Must be :user, :agent, :tool_request, or :tool_result." unless %i[user agent tool_request tool_result].include?(role)

    Legate.logger.warn("Event: :#{role} event created without a valid :tool_name symbol.") if %i[tool_request tool_result].include?(role) && (tool_name.nil? || !tool_name.is_a?(Symbol))

    # Validate state_delta is a Hash or nil
    unless state_delta.nil? || state_delta.is_a?(Hash)
      Legate.logger.warn("Event: :state_delta must be a Hash or nil, received #{state_delta.class}.")
      state_delta = nil # Force to nil if invalid
    end

    # Ensure content is somewhat reasonable (avoids deep inspection for performance)
    Legate.logger.warn("Event: Content is of unusual type (#{content.class}): #{content.inspect}") unless content.is_a?(String) || content.is_a?(Hash) || content.is_a?(Array) || content.is_a?(NilClass) || content.is_a?(Numeric) || content.is_a?(TrueClass) || content.is_a?(FalseClass)

    super(
      role: role,
      content: deep_freeze(content),
      timestamp: timestamp || Time.now.utc,
      tool_name: tool_name,
      state_delta: deep_freeze(state_delta&.transform_keys(&:to_sym)),
      event_id: event_id || SecureRandom.uuid
    )
    freeze
  end

  private

  def deep_freeze(obj)
    case obj
    when Hash
      obj.each_value { |v| deep_freeze(v) }
      obj.freeze
    when Array
      obj.each { |v| deep_freeze(v) }
      obj.freeze
    when String
      obj.freeze
    else
      obj
    end
  end

  public

  # Helper to check if the event represents a final agent response to the user.
  # @return [Boolean]
  def final_agent_response?
    role == :agent
  end

  # --- Result accessors ---
  # Convenience readers over the standard { status:, result: / error_message: }
  # content hash, so callers don't reach into it. Meaningful on a final agent
  # event (e.g. the return of Agent#ask / #run_task); harmless elsewhere.

  # @return [Boolean] true if this carries a successful result
  def success?
    content.is_a?(Hash) && content[:status] == :success
  end

  # @return [Boolean] true if this carries an error result
  def error?
    content.is_a?(Hash) && content[:status] == :error
  end

  # The successful result value (nil on error). Non-Hash content is returned
  # as-is (e.g. a scalar result stored directly).
  # @return [Object, nil]
  def answer
    content.is_a?(Hash) ? content[:result] : content
  end

  # @return [String, nil] the error message, or nil when not an error
  def error_message
    content.is_a?(Hash) ? content[:error_message] : nil
  end

  # Basic serialization for storage (e.g., in Redis).
  # @return [Hash] A hash representation suitable for JSON conversion.
  def to_h
    {
      role: role,
      content: content, # Assumes content is already JSON-serializable
      timestamp: timestamp.iso8601(3), # Use ISO8601 format with milliseconds
      tool_name: tool_name,
      state_delta: state_delta, # Store the hash directly (must be JSON-serializable)
      event_id: event_id
    }
  end

  # Basic deserialization from a hash (e.g., after reading from JSON).
  # @param hash [Hash] The hash containing event data (uses symbolized keys).
  # @return [Legate::Event] A new Event object.
  def self.from_h(hash)
    # Optimized: Extract fields manually to avoid full hash allocation via transform_keys
    role = hash.key?(:role) ? hash[:role] : hash['role']
    content = hash.key?(:content) ? hash[:content] : hash['content']
    ts_val = hash.key?(:timestamp) ? hash[:timestamp] : hash['timestamp']
    tool_name = hash.key?(:tool_name) ? hash[:tool_name] : hash['tool_name']
    state_delta = hash.key?(:state_delta) ? hash[:state_delta] : hash['state_delta']

    # Validate state_delta type to preserve strict behavior (fail on invalid type)
    if state_delta && !state_delta.is_a?(Hash)
      Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): state_delta must be a Hash. Hash: #{hash.inspect}")
      return nil
    end

    event_id = hash.key?(:event_id) ? hash[:event_id] : hash['event_id']

    new(
      role: role&.to_sym,
      content: content,
      # Safely parse timestamp
      timestamp: ts_val ? Time.iso8601(ts_val) : Time.now.utc,
      tool_name: tool_name&.to_sym,
      # Pass state_delta directly; initialize handles validation and symbolization/copy
      state_delta: state_delta,
      event_id: event_id
    )
  rescue ArgumentError => e
    Legate.logger.error("Event.from_h: Failed to parse timestamp or invalid role: #{e.message}. Hash: #{hash.inspect}")
    # Decide on fallback: return nil, raise, or return partial object?
    # Returning nil might be safest to signal deserialization failure.
    nil
  rescue TypeError, NoMethodError => e # Also rescue NoMethodError
    Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): #{e.message}. Hash: #{hash.inspect}")
    nil
  end
end

#timestampTime (readonly)

Returns The UTC time the event occurred.

Returns:

  • (Time)

    The UTC time the event occurred.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/legate/event.rb', line 24

Event = Struct.new(:role, :content, :timestamp, :tool_name, :state_delta, :event_id, keyword_init: true) do
  # @param role [Symbol] :user, :agent, :tool_request, :tool_result
  # @param content [String, Hash] Event payload. Should be JSON-serializable.
  # @param timestamp [Time, nil] Timestamp (defaults to Time.now.utc).
  # @param tool_name [Symbol, nil] Name of the tool if role is tool related.
  # @param state_delta [Hash, nil] State changes to apply with this event.
  # @param event_id [String, nil] Unique event ID (defaults to SecureRandom.uuid).
  def initialize(role:, content:, timestamp: nil, tool_name: nil, state_delta: nil, event_id: nil)
    # Basic validation
    raise ArgumentError, "Invalid role: #{role}. Must be :user, :agent, :tool_request, or :tool_result." unless %i[user agent tool_request tool_result].include?(role)

    Legate.logger.warn("Event: :#{role} event created without a valid :tool_name symbol.") if %i[tool_request tool_result].include?(role) && (tool_name.nil? || !tool_name.is_a?(Symbol))

    # Validate state_delta is a Hash or nil
    unless state_delta.nil? || state_delta.is_a?(Hash)
      Legate.logger.warn("Event: :state_delta must be a Hash or nil, received #{state_delta.class}.")
      state_delta = nil # Force to nil if invalid
    end

    # Ensure content is somewhat reasonable (avoids deep inspection for performance)
    Legate.logger.warn("Event: Content is of unusual type (#{content.class}): #{content.inspect}") unless content.is_a?(String) || content.is_a?(Hash) || content.is_a?(Array) || content.is_a?(NilClass) || content.is_a?(Numeric) || content.is_a?(TrueClass) || content.is_a?(FalseClass)

    super(
      role: role,
      content: deep_freeze(content),
      timestamp: timestamp || Time.now.utc,
      tool_name: tool_name,
      state_delta: deep_freeze(state_delta&.transform_keys(&:to_sym)),
      event_id: event_id || SecureRandom.uuid
    )
    freeze
  end

  private

  def deep_freeze(obj)
    case obj
    when Hash
      obj.each_value { |v| deep_freeze(v) }
      obj.freeze
    when Array
      obj.each { |v| deep_freeze(v) }
      obj.freeze
    when String
      obj.freeze
    else
      obj
    end
  end

  public

  # Helper to check if the event represents a final agent response to the user.
  # @return [Boolean]
  def final_agent_response?
    role == :agent
  end

  # --- Result accessors ---
  # Convenience readers over the standard { status:, result: / error_message: }
  # content hash, so callers don't reach into it. Meaningful on a final agent
  # event (e.g. the return of Agent#ask / #run_task); harmless elsewhere.

  # @return [Boolean] true if this carries a successful result
  def success?
    content.is_a?(Hash) && content[:status] == :success
  end

  # @return [Boolean] true if this carries an error result
  def error?
    content.is_a?(Hash) && content[:status] == :error
  end

  # The successful result value (nil on error). Non-Hash content is returned
  # as-is (e.g. a scalar result stored directly).
  # @return [Object, nil]
  def answer
    content.is_a?(Hash) ? content[:result] : content
  end

  # @return [String, nil] the error message, or nil when not an error
  def error_message
    content.is_a?(Hash) ? content[:error_message] : nil
  end

  # Basic serialization for storage (e.g., in Redis).
  # @return [Hash] A hash representation suitable for JSON conversion.
  def to_h
    {
      role: role,
      content: content, # Assumes content is already JSON-serializable
      timestamp: timestamp.iso8601(3), # Use ISO8601 format with milliseconds
      tool_name: tool_name,
      state_delta: state_delta, # Store the hash directly (must be JSON-serializable)
      event_id: event_id
    }
  end

  # Basic deserialization from a hash (e.g., after reading from JSON).
  # @param hash [Hash] The hash containing event data (uses symbolized keys).
  # @return [Legate::Event] A new Event object.
  def self.from_h(hash)
    # Optimized: Extract fields manually to avoid full hash allocation via transform_keys
    role = hash.key?(:role) ? hash[:role] : hash['role']
    content = hash.key?(:content) ? hash[:content] : hash['content']
    ts_val = hash.key?(:timestamp) ? hash[:timestamp] : hash['timestamp']
    tool_name = hash.key?(:tool_name) ? hash[:tool_name] : hash['tool_name']
    state_delta = hash.key?(:state_delta) ? hash[:state_delta] : hash['state_delta']

    # Validate state_delta type to preserve strict behavior (fail on invalid type)
    if state_delta && !state_delta.is_a?(Hash)
      Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): state_delta must be a Hash. Hash: #{hash.inspect}")
      return nil
    end

    event_id = hash.key?(:event_id) ? hash[:event_id] : hash['event_id']

    new(
      role: role&.to_sym,
      content: content,
      # Safely parse timestamp
      timestamp: ts_val ? Time.iso8601(ts_val) : Time.now.utc,
      tool_name: tool_name&.to_sym,
      # Pass state_delta directly; initialize handles validation and symbolization/copy
      state_delta: state_delta,
      event_id: event_id
    )
  rescue ArgumentError => e
    Legate.logger.error("Event.from_h: Failed to parse timestamp or invalid role: #{e.message}. Hash: #{hash.inspect}")
    # Decide on fallback: return nil, raise, or return partial object?
    # Returning nil might be safest to signal deserialization failure.
    nil
  rescue TypeError, NoMethodError => e # Also rescue NoMethodError
    Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): #{e.message}. Hash: #{hash.inspect}")
    nil
  end
end

#tool_nameSymbol? (readonly)

Returns The name of the tool involved (for :tool_request, :tool_result roles).

Returns:

  • (Symbol, nil)

    The name of the tool involved (for :tool_request, :tool_result roles).



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/legate/event.rb', line 24

Event = Struct.new(:role, :content, :timestamp, :tool_name, :state_delta, :event_id, keyword_init: true) do
  # @param role [Symbol] :user, :agent, :tool_request, :tool_result
  # @param content [String, Hash] Event payload. Should be JSON-serializable.
  # @param timestamp [Time, nil] Timestamp (defaults to Time.now.utc).
  # @param tool_name [Symbol, nil] Name of the tool if role is tool related.
  # @param state_delta [Hash, nil] State changes to apply with this event.
  # @param event_id [String, nil] Unique event ID (defaults to SecureRandom.uuid).
  def initialize(role:, content:, timestamp: nil, tool_name: nil, state_delta: nil, event_id: nil)
    # Basic validation
    raise ArgumentError, "Invalid role: #{role}. Must be :user, :agent, :tool_request, or :tool_result." unless %i[user agent tool_request tool_result].include?(role)

    Legate.logger.warn("Event: :#{role} event created without a valid :tool_name symbol.") if %i[tool_request tool_result].include?(role) && (tool_name.nil? || !tool_name.is_a?(Symbol))

    # Validate state_delta is a Hash or nil
    unless state_delta.nil? || state_delta.is_a?(Hash)
      Legate.logger.warn("Event: :state_delta must be a Hash or nil, received #{state_delta.class}.")
      state_delta = nil # Force to nil if invalid
    end

    # Ensure content is somewhat reasonable (avoids deep inspection for performance)
    Legate.logger.warn("Event: Content is of unusual type (#{content.class}): #{content.inspect}") unless content.is_a?(String) || content.is_a?(Hash) || content.is_a?(Array) || content.is_a?(NilClass) || content.is_a?(Numeric) || content.is_a?(TrueClass) || content.is_a?(FalseClass)

    super(
      role: role,
      content: deep_freeze(content),
      timestamp: timestamp || Time.now.utc,
      tool_name: tool_name,
      state_delta: deep_freeze(state_delta&.transform_keys(&:to_sym)),
      event_id: event_id || SecureRandom.uuid
    )
    freeze
  end

  private

  def deep_freeze(obj)
    case obj
    when Hash
      obj.each_value { |v| deep_freeze(v) }
      obj.freeze
    when Array
      obj.each { |v| deep_freeze(v) }
      obj.freeze
    when String
      obj.freeze
    else
      obj
    end
  end

  public

  # Helper to check if the event represents a final agent response to the user.
  # @return [Boolean]
  def final_agent_response?
    role == :agent
  end

  # --- Result accessors ---
  # Convenience readers over the standard { status:, result: / error_message: }
  # content hash, so callers don't reach into it. Meaningful on a final agent
  # event (e.g. the return of Agent#ask / #run_task); harmless elsewhere.

  # @return [Boolean] true if this carries a successful result
  def success?
    content.is_a?(Hash) && content[:status] == :success
  end

  # @return [Boolean] true if this carries an error result
  def error?
    content.is_a?(Hash) && content[:status] == :error
  end

  # The successful result value (nil on error). Non-Hash content is returned
  # as-is (e.g. a scalar result stored directly).
  # @return [Object, nil]
  def answer
    content.is_a?(Hash) ? content[:result] : content
  end

  # @return [String, nil] the error message, or nil when not an error
  def error_message
    content.is_a?(Hash) ? content[:error_message] : nil
  end

  # Basic serialization for storage (e.g., in Redis).
  # @return [Hash] A hash representation suitable for JSON conversion.
  def to_h
    {
      role: role,
      content: content, # Assumes content is already JSON-serializable
      timestamp: timestamp.iso8601(3), # Use ISO8601 format with milliseconds
      tool_name: tool_name,
      state_delta: state_delta, # Store the hash directly (must be JSON-serializable)
      event_id: event_id
    }
  end

  # Basic deserialization from a hash (e.g., after reading from JSON).
  # @param hash [Hash] The hash containing event data (uses symbolized keys).
  # @return [Legate::Event] A new Event object.
  def self.from_h(hash)
    # Optimized: Extract fields manually to avoid full hash allocation via transform_keys
    role = hash.key?(:role) ? hash[:role] : hash['role']
    content = hash.key?(:content) ? hash[:content] : hash['content']
    ts_val = hash.key?(:timestamp) ? hash[:timestamp] : hash['timestamp']
    tool_name = hash.key?(:tool_name) ? hash[:tool_name] : hash['tool_name']
    state_delta = hash.key?(:state_delta) ? hash[:state_delta] : hash['state_delta']

    # Validate state_delta type to preserve strict behavior (fail on invalid type)
    if state_delta && !state_delta.is_a?(Hash)
      Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): state_delta must be a Hash. Hash: #{hash.inspect}")
      return nil
    end

    event_id = hash.key?(:event_id) ? hash[:event_id] : hash['event_id']

    new(
      role: role&.to_sym,
      content: content,
      # Safely parse timestamp
      timestamp: ts_val ? Time.iso8601(ts_val) : Time.now.utc,
      tool_name: tool_name&.to_sym,
      # Pass state_delta directly; initialize handles validation and symbolization/copy
      state_delta: state_delta,
      event_id: event_id
    )
  rescue ArgumentError => e
    Legate.logger.error("Event.from_h: Failed to parse timestamp or invalid role: #{e.message}. Hash: #{hash.inspect}")
    # Decide on fallback: return nil, raise, or return partial object?
    # Returning nil might be safest to signal deserialization failure.
    nil
  rescue TypeError, NoMethodError => e # Also rescue NoMethodError
    Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): #{e.message}. Hash: #{hash.inspect}")
    nil
  end
end

Class Method Details

.from_h(hash) ⇒ Legate::Event

Basic deserialization from a hash (e.g., after reading from JSON).

Parameters:

  • hash (Hash)

    The hash containing event data (uses symbolized keys).

Returns:



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/legate/event.rb', line 125

def self.from_h(hash)
  # Optimized: Extract fields manually to avoid full hash allocation via transform_keys
  role = hash.key?(:role) ? hash[:role] : hash['role']
  content = hash.key?(:content) ? hash[:content] : hash['content']
  ts_val = hash.key?(:timestamp) ? hash[:timestamp] : hash['timestamp']
  tool_name = hash.key?(:tool_name) ? hash[:tool_name] : hash['tool_name']
  state_delta = hash.key?(:state_delta) ? hash[:state_delta] : hash['state_delta']

  # Validate state_delta type to preserve strict behavior (fail on invalid type)
  if state_delta && !state_delta.is_a?(Hash)
    Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): state_delta must be a Hash. Hash: #{hash.inspect}")
    return nil
  end

  event_id = hash.key?(:event_id) ? hash[:event_id] : hash['event_id']

  new(
    role: role&.to_sym,
    content: content,
    # Safely parse timestamp
    timestamp: ts_val ? Time.iso8601(ts_val) : Time.now.utc,
    tool_name: tool_name&.to_sym,
    # Pass state_delta directly; initialize handles validation and symbolization/copy
    state_delta: state_delta,
    event_id: event_id
  )
rescue ArgumentError => e
  Legate.logger.error("Event.from_h: Failed to parse timestamp or invalid role: #{e.message}. Hash: #{hash.inspect}")
  # Decide on fallback: return nil, raise, or return partial object?
  # Returning nil might be safest to signal deserialization failure.
  nil
rescue TypeError, NoMethodError => e # Also rescue NoMethodError
  Legate.logger.error("Event.from_h: Type error during deserialization (check state_delta?): #{e.message}. Hash: #{hash.inspect}")
  nil
end

Instance Method Details

#answerObject?

The successful result value (nil on error). Non-Hash content is returned as-is (e.g. a scalar result stored directly).

Returns:

  • (Object, nil)


100
101
102
# File 'lib/legate/event.rb', line 100

def answer
  content.is_a?(Hash) ? content[:result] : content
end

#error?Boolean

Returns true if this carries an error result.

Returns:

  • (Boolean)

    true if this carries an error result



93
94
95
# File 'lib/legate/event.rb', line 93

def error?
  content.is_a?(Hash) && content[:status] == :error
end

#error_messageString?

Returns the error message, or nil when not an error.

Returns:

  • (String, nil)

    the error message, or nil when not an error



105
106
107
# File 'lib/legate/event.rb', line 105

def error_message
  content.is_a?(Hash) ? content[:error_message] : nil
end

#final_agent_response?Boolean

Helper to check if the event represents a final agent response to the user.

Returns:

  • (Boolean)


78
79
80
# File 'lib/legate/event.rb', line 78

def final_agent_response?
  role == :agent
end

#success?Boolean

Returns true if this carries a successful result.

Returns:

  • (Boolean)

    true if this carries a successful result



88
89
90
# File 'lib/legate/event.rb', line 88

def success?
  content.is_a?(Hash) && content[:status] == :success
end

#to_hHash

Basic serialization for storage (e.g., in Redis).

Returns:

  • (Hash)

    A hash representation suitable for JSON conversion.



111
112
113
114
115
116
117
118
119
120
# File 'lib/legate/event.rb', line 111

def to_h
  {
    role: role,
    content: content, # Assumes content is already JSON-serializable
    timestamp: timestamp.iso8601(3), # Use ISO8601 format with milliseconds
    tool_name: tool_name,
    state_delta: state_delta, # Store the hash directly (must be JSON-serializable)
    event_id: event_id
  }
end