Class: Legate::Session

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

Overview

Represents a single, ongoing conversation thread between a user and an agent system. It holds the history of interactions (Events) and temporary session-specific data (State).

Constant Summary collapse

VALID_PREFIXES =
%w[user app temp].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app_name:, user_id:, id: nil, initial_state: {}, events: [], session_service: nil) ⇒ Session

Initializes a new session. Typically called by a SessionService.

Parameters:

  • id (String) (defaults to: nil)

    A unique identifier for the session (defaults to UUID).

  • app_name (String)

    Identifier for the agent application.

  • user_id (String)

    Identifier for the user initiating the session.

  • initial_state (Hash) (defaults to: {})

    Optional initial data for the session state. Keys are symbolized.

  • events (Array<Legate::Event>) (defaults to: [])

    Optional initial list of events (for reloading).

  • session_service (Legate::SessionService::Base) (defaults to: nil)

    The session service to use for persistence



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
# File 'lib/legate/session.rb', line 26

def initialize(app_name:, user_id:, id: nil, initial_state: {}, events: [], session_service: nil)
  @id = id || SecureRandom.uuid
  @app_name = app_name
  @user_id = user_id
  @created_at = Time.now.utc # Use UTC
  @updated_at = @created_at
  @session_service = session_service
  @mutex = Mutex.new
  # Use Concurrent::Map for thread-safe state storage within the session object itself
  @state = Concurrent::Map.new
  # Ensure initial_state keys are symbols and manually populate the map
  initial_state = {} unless initial_state.is_a?(Hash) # Ensure it's a hash
  symbolized_initial_state = initial_state.transform_keys { |k|
    begin
      k.to_sym
    rescue StandardError
      k
    end
  }
  symbolized_initial_state.each_pair do |key, value|
    @state[key] = value
  end
  # Events array stores the history, ensure it's mutable if passed and validate contents
  @events = events.map do |e|
    if e.is_a?(Legate::Event)
      e
    else
      (Legate.logger.warn("Session Init: Invalid event data skipped: #{e.inspect}")
       nil)
    end
  end.compact
  Legate.logger.debug("Session initialized: id=#{@id}, app=#{@app_name}, user=#{@user_id}, event_count=#{@events.size}")
end

Instance Attribute Details

#app_nameObject (readonly)

Returns the value of attribute app_name.



16
17
18
# File 'lib/legate/session.rb', line 16

def app_name
  @app_name
end

#created_atObject (readonly)

Returns the value of attribute created_at.



16
17
18
# File 'lib/legate/session.rb', line 16

def created_at
  @created_at
end

#idObject (readonly)

Returns the value of attribute id.



16
17
18
# File 'lib/legate/session.rb', line 16

def id
  @id
end

#session_serviceObject

Returns the value of attribute session_service.



17
18
19
# File 'lib/legate/session.rb', line 17

def session_service
  @session_service
end

#updated_atObject

Returns the value of attribute updated_at.



17
18
19
# File 'lib/legate/session.rb', line 17

def updated_at
  @updated_at
end

#user_idObject (readonly)

Returns the value of attribute user_id.



16
17
18
# File 'lib/legate/session.rb', line 16

def user_id
  @user_id
end

Class Method Details

.from_h(hash) ⇒ Legate::Session

Deserializes session data from a hash into a Session object.

Parameters:

  • hash (Hash)

    Hash containing session data (typically from JSON).

Returns:



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/legate/session.rb', line 202

def self.from_h(hash)
  sym_hash = hash.transform_keys(&:to_sym) # Ensure keys are symbols
  events_data = sym_hash[:events] || []
  events = events_data.map { |event_hash| Legate::Event.from_h(event_hash.transform_keys(&:to_sym)) }.compact

  new(
    id: sym_hash[:id],
    app_name: sym_hash[:app_name],
    user_id: sym_hash[:user_id],
    initial_state: sym_hash[:state] || {},
    events: events
  ).tap do |session|
    # Set timestamps after initialization
    session.instance_variable_set(:@created_at, Time.iso8601(sym_hash[:created_at])) if sym_hash[:created_at]
    session.instance_variable_set(:@updated_at, Time.iso8601(sym_hash[:updated_at])) if sym_hash[:updated_at]
  end
rescue ArgumentError, TypeError => e
  Legate.logger.error("Session.from_h: Failed to deserialize session data. Error: #{e.message}. Data: #{hash.inspect}")
  nil # Return nil on deserialization error
end

Instance Method Details

#add_event(event) ⇒ Legate::Event?

Adds an event to the session’s history and updates state if needed.

Parameters:

Returns:



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/legate/session.rb', line 76

def add_event(event)
  return nil unless event.is_a?(Legate::Event)

  @mutex.synchronize do
    @events << event
    @updated_at = Time.now.utc
  end

  # Apply the event's state delta OUTSIDE the mutex: update_state may call the
  # session service (save_scoped_state), and holding the non-reentrant @mutex
  # across that external call risks deadlock if a service implementation calls
  # back into the session (e.g. #events/#to_h). @state is a Concurrent::Map, so
  # it is safe without the lock.
  update_state(event.state_delta) if event.state_delta && !event.state_delta.empty?

  Legate.logger.debug("Session #{@id}: Event added - Role: #{event.role}, Tool: #{event.tool_name || 'N/A'}")
  event
end

#clear_state!Object

Clears all key-value pairs from the session state.



167
168
169
170
171
172
173
# File 'lib/legate/session.rb', line 167

def clear_state!
  touch!
  @state.clear
  VALID_PREFIXES.each do |prefix|
    @session_service&.clear_scoped_state(scoped_namespace(prefix), '*')
  end
end

#delete_state(key) ⇒ Object?

Deletes a key from the session state.

Parameters:

  • key (Symbol, String)

    The key to delete.

Returns:

  • (Object, nil)

    The value of the deleted key, or nil if not found.



154
155
156
157
158
159
160
161
162
163
164
# File 'lib/legate/session.rb', line 154

def delete_state(key)
  prefix, real_key = parse_key(key)
  validate_prefix!(prefix) if prefix

  touch!
  if prefix
    @session_service&.clear_scoped_state(scoped_namespace(prefix), real_key)
  else
    @state.delete(real_key.to_sym)
  end
end

#eventsArray<Legate::Event>

Thread-safe accessor for session events.

Returns:

  • (Array<Legate::Event>)

    A frozen snapshot of the event history.



62
63
64
# File 'lib/legate/session.rb', line 62

def events
  @mutex.synchronize { @events.dup.freeze }
end

#get_state(key) ⇒ Object?

Gets a value from the session state.

Parameters:

  • key (Symbol, String)

    The key to retrieve.

Returns:

  • (Object, nil)

    The value associated with the key, or nil if not found.



100
101
102
103
104
105
106
107
108
# File 'lib/legate/session.rb', line 100

def get_state(key)
  prefix, real_key = parse_key(key)
  validate_prefix!(prefix) if prefix # match set/update/delete: reads and writes agree on what a key means
  if prefix
    @session_service&.load_scoped_state(scoped_namespace(prefix), real_key)
  else
    @state[real_key.to_sym]
  end
end

#set_state(key, value) ⇒ Object

Sets a value in the session state.

Parameters:

  • key (Symbol, String)

    The key to set.

  • value (Object)

    The value to store.

Returns:

  • (Object)

    The value that was set.

Raises:

  • (Legate::StateValidationError)

    If the value cannot be serialized

  • (Legate::InvalidPrefixError)

    If an invalid prefix is used



116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/legate/session.rb', line 116

def set_state(key, value)
  validate_serializable!(value)
  prefix, real_key = parse_key(key)
  validate_prefix!(prefix) if prefix

  touch!
  if prefix
    @session_service&.save_scoped_state(scoped_namespace(prefix), real_key, value)
  else
    @state[real_key.to_sym] = value
  end
  value
end

#stateHash

Provides access to the session’s temporary state data.

Returns:

  • (Hash)

    The current session state (immutable view).



68
69
70
71
# File 'lib/legate/session.rb', line 68

def state
  # Ensure external modifications don't affect internal state directly.
  @state.dup # Return a shallow copy
end

#state_to_hHash

Provides a plain Hash representation of the current state.

Returns:

  • (Hash)

    A copy of the session state.



177
178
179
180
# File 'lib/legate/session.rb', line 177

def state_to_h
  # Convert Concurrent::Map to a regular Hash
  Hash[@state.to_enum(:each_pair).to_a]
end

#to_hHash

Serializes the entire session object to a Hash suitable for JSON conversion.

Returns:

  • (Hash)

    Hash representation of the session.



186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/legate/session.rb', line 186

def to_h
  serialized_events = @mutex.synchronize { @events.map(&:to_h) }
  {
    id: @id,
    app_name: @app_name,
    user_id: @user_id,
    created_at: @created_at.iso8601(3),
    updated_at: @updated_at.iso8601(3),
    state: state_to_h,
    events: serialized_events
  }
end

#update_state(hash) ⇒ Object

Merges a hash into the session state.

Parameters:

  • hash (Hash)

    The hash to merge into the state.

Raises:

  • (Legate::StateValidationError)

    If any value cannot be serialized

  • (Legate::InvalidPrefixError)

    If any key has an invalid prefix



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/legate/session.rb', line 134

def update_state(hash)
  return unless hash.is_a?(Hash)

  touch!
  hash.each do |k, v|
    validate_serializable!(v)
    prefix, real_key = parse_key(k)
    validate_prefix!(prefix) if prefix

    if prefix
      @session_service&.save_scoped_state(scoped_namespace(prefix), real_key, v)
    else
      @state[real_key.to_sym] = v
    end
  end
end