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 is the checkpoint boundary.

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.



41
42
43
44
45
46
47
48
# File 'lib/little_ghost/session.rb', line 41

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 = .to_h.freeze
  @loaded = false
end

Instance Attribute Details

#actor_idObject (readonly)

The store key, explicit actor identity, backing store, and telemetry operation used by this session.



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

def actor_id
  @actor_id
end

#idObject (readonly)

The store key, explicit actor identity, backing store, and telemetry operation used by this session.



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

def id
  @id
end

#operation_idObject (readonly)

The store key, explicit actor identity, backing store, and telemetry operation used by this session.



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

def operation_id
  @operation_id
end

#storeObject (readonly)

The store key, explicit actor identity, backing store, and telemetry operation used by this session.



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

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.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/little_ghost/session.rb', line 81

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.



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/little_ghost/session.rb', line 111

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.



135
136
137
# File 'lib/little_ghost/session.rb', line 135

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.



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

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

#loadObject

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



51
52
53
54
55
56
57
58
# File 'lib/little_ghost/session.rb', line 51

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.



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

def 
  load&.fetch(:metadata) || @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.



149
150
151
152
153
# File 'lib/little_ghost/session.rb', line 149

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.



103
104
105
106
107
# File 'lib/little_ghost/session.rb', line 103

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 copy of the persisted application state.



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

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

#synchronize(&block) ⇒ Object

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



140
141
142
# File 'lib/little_ghost/session.rb', line 140

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