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 = DataMap.new().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.



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

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.



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

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.



137
138
139
# File 'lib/little_ghost/session.rb', line 137

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. The returned DataMap is frozen.



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

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.



151
152
153
154
155
# File 'lib/little_ghost/session.rb', line 151

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.



105
106
107
108
109
# File 'lib/little_ghost/session.rb', line 105

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.



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

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.



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

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