Class: LittleGhost::Session

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

Overview

Sessions let an agent continue a conversation without tying it to one Ruby process. Each session keeps messages, application state, and metadata together behind a SessionStore.

session = LittleGhost::Session.new(
id: "conversation-42",
actor_id: "user-7",
store: LittleGhost::SessionStores::Memory.new
)
session.append(
messages: [LittleGhost::Message.new(role: :user, content: "Hello")],
state: {language: "en"}
)

reopened = LittleGhost::Session.new(
id: "conversation-42",
actor_id: "user-7",
store: session.store
)
reopened.history.last.text # => "Hello"
reopened.state[:language]  # => "en"

Persistence and trust

System messages, transient messages, and private model reasoning are removed before persistence. Store failures reach the caller. A successful write becomes the checkpoint used by later updates.

Multi-tenant applications must derive actor_id from stable, authenticated identity. A nil actor provides no tenant isolation and is appropriate only for a store that serves one actor.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(id:, store:, actor_id: nil, metadata: {}, operation_id: nil) ⇒ Session

No store access occurs until the session is read or written.



46
47
48
49
50
51
52
53
# File 'lib/little_ghost/session.rb', line 46

def initialize(id:, store:, actor_id: nil, metadata: {}, operation_id: nil)
  @id = String(id)
  @actor_id = actor_id&.to_s
  @store = store
  @operation_id = operation_id
  @metadata = DataMap.new().freeze
  @loaded = false
end

Instance Attribute Details

#actor_idObject (readonly)

The trusted application identity that owns this Session, when supplied.



39
40
41
# File 'lib/little_ghost/session.rb', line 39

def actor_id
  @actor_id
end

#idObject (readonly)

The key used to load and save this Session.



37
38
39
# File 'lib/little_ghost/session.rb', line 37

def id
  @id
end

#operation_idObject (readonly)

The telemetry operation associated with this Session, when supplied.



43
44
45
# File 'lib/little_ghost/session.rb', line 43

def operation_id
  @operation_id
end

#storeObject (readonly)

The SessionStore that loads and saves snapshots.



41
42
43
# File 'lib/little_ghost/session.rb', line 41

def store
  @store
end

Instance Method Details

#append(messages:, state: self.state, metadata: self.metadata) ⇒ Object

Atomically appends messages when the store still has the expected history length. Prefer #checkpoint when replacing earlier messages is also valid.



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/little_ghost/session.rb', line 88

def append(messages:, state: self.state, metadata: self.)
  current = current_snapshot
  added = persistable_messages(messages)
  snapshot = build_snapshot(
    messages: [*current.fetch(:messages), *added],
    state:,
    metadata:
  )
  with_store_operation_context do
    store.append(
      id,
      messages: added,
      state: snapshot.fetch(:state),
      metadata: snapshot.fetch(:metadata),
      expected_count: current.fetch(:messages).length,
      actor_id:
    )
  end
  remember(snapshot)
end

#checkpoint(messages:, state: self.state, metadata: self.metadata, parent_operation_id: @operation_id) ⇒ Object

Persists one conversation checkpoint. History is appended when the stored messages are an unchanged prefix and replaced otherwise.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/little_ghost/session.rb', line 118

def checkpoint(messages:, state: self.state, metadata: self., parent_operation_id: @operation_id)
  with_store_operation_context(parent_operation_id) do
    snapshot = build_snapshot(messages:, state:, metadata:)
    current = current_snapshot
    if message_prefix?(current.fetch(:messages), snapshot.fetch(:messages))
      added = snapshot.fetch(:messages).drop(current.fetch(:messages).length)
      unless added.empty? && same_session_data?(current, snapshot)
        store.append(
          id,
          messages: added,
          state: snapshot.fetch(:state),
          metadata: snapshot.fetch(:metadata),
          expected_count: current.fetch(:messages).length,
          actor_id:
        )
      end
    else
      store.replace(id, actor_id:, **snapshot)
    end
    remember(snapshot)
  end
end

#checkpoint_result(result) ⇒ Object

Checkpoints the messages and state from a completed run result.



142
143
144
# File 'lib/little_ghost/session.rb', line 142

def checkpoint_result(result)
  checkpoint(messages: result.messages, state: result.state)
end

#history(fallback: []) ⇒ Object

Uses persisted conversation messages when present and fallback for a new session.



67
68
69
# File 'lib/little_ghost/session.rb', line 67

def history(fallback: [])
  load&.fetch(:messages) || fallback
end

#loadObject

Loads and normalizes the snapshot once. A new session has no snapshot.



56
57
58
59
60
61
62
63
# File 'lib/little_ghost/session.rb', line 56

def load
  return @snapshot if @loaded

  value = with_store_operation_context { store.load(id, actor_id:) }
  @snapshot = normalize(value)
  @loaded = true
  @snapshot
end

#metadataObject

Uses persisted metadata when present and otherwise keeps the metadata from construction. The returned DataMap is frozen.



80
81
82
83
# File 'lib/little_ghost/session.rb', line 80

def 
  loaded = load
  loaded ? DataMap.new(loaded.fetch(:metadata)).freeze : @metadata
end

#project_conversation(messages:, metadata: self.metadata) ⇒ Object

Publishes a conversational view without changing the session's stored transcript. Unlike session persistence, projection does not automatically remove system or transient messages; callers must omit any message whose visible text should stay local. Stores that do not support projections return nil.



156
157
158
159
160
# File 'lib/little_ghost/session.rb', line 156

def project_conversation(messages:, metadata: self.)
  with_store_operation_context do
    store.project_conversation(id, messages:, metadata:, actor_id:)
  end
end

#replace(messages:, state: self.state, metadata: self.metadata) ⇒ Object

Replaces the complete persisted snapshot.



110
111
112
113
114
# File 'lib/little_ghost/session.rb', line 110

def replace(messages:, state: self.state, metadata: self.)
  snapshot = build_snapshot(messages:, state:, metadata:)
  with_store_operation_context { store.replace(id, actor_id:, **snapshot) }
  remember(snapshot)
end

#stateObject

Exposes a mutable DataMap copy of the persisted application state. String and Symbol keys address the same value; persisted snapshots use Strings.



73
74
75
76
# File 'lib/little_ghost/session.rb', line 73

def state
  snapshot = load
  DataMap.new(snapshot ? snapshot.fetch(:state) : {})
end

#synchronize(&block) ⇒ Object

Serializes work for this session and actor through the backing store.



147
148
149
# File 'lib/little_ghost/session.rb', line 147

def synchronize(&block)
  store.synchronize(id, actor_id:, &block)
end