Class: Terret::Sessions

Inherits:
Hames::Service
  • Object
show all
Defined in:
lib/terret/sessions.rb

Overview

ctx.sessions — the append-only session log. The single source of the context the model sees: derive_messages projects model history from it, and a digest invariant asserts that what goes out to an adapter is exactly what replaying the log yields ("model-visible means logged").

Defined Under Namespace

Classes: Session

Instance Method Summary collapse

Instance Method Details

#append(session_id, type, payload = {}) ⇒ Object

Raises:

  • (Hames::ContractError)


36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/terret/sessions.rb', line 36

def append(session_id, type, payload = {})
  decl = Hames.event(type)
  raise Hames::ContractError, "#{type} is not a durable event" unless decl.durable

  s = fetch(session_id)
  ev = SessionEvent.new(
    id: SecureRandom.hex(8), session_id:, seq: s.events.length,
    at: Time.now.utc, type: type.to_s, payload: payload
  )
  s.events << ev
  persist(ev)
  @ctx.emit("session/event", ev)
  ev
end

#assert_log_invariant!(session_id, outbound_messages) ⇒ Object

The enforcement point for "model-visible means logged": the loop calls this with the message list it is about to send; a mismatch against the log projection raises in dev/test.



73
74
75
76
77
78
79
# File 'lib/terret/sessions.rb', line 73

def assert_log_invariant!(session_id, outbound_messages)
  derived = derive_messages(session_id)
  return if digest(derived) == digest(outbound_messages)

  raise LogInvariantViolation,
        "outbound request diverges from session log projection for #{session_id}"
end

#create(id: SecureRandom.hex(6), parent_id: nil) ⇒ Object



27
28
29
30
31
32
# File 'lib/terret/sessions.rb', line 27

def create(id: SecureRandom.hex(6), parent_id: nil)
  s = Session.new(id:, events: [], parent_id:)
  @store[id] = s
  append(id, "session/created", { parent_id: })
  s
end

#derive_messages(session_id, upto: nil) ⇒ Object

Project provider-neutral model history from the durable log.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/terret/sessions.rb', line 52

def derive_messages(session_id, upto: nil)
  events = fetch(session_id).events
  events = events.take(upto) if upto
  events.filter_map do |ev|
    case ev.type
    when "user/message", "context/injected"
      LLM::Message.new(role: :user, parts: [LLM::Text.new(text: ev.payload[:text])])
    when "assistant/message"
      LLM::Message.new(role: :assistant, parts: ev.payload[:parts])
    when "tool/result"
      LLM::Message.new(role: :tool, parts: [
        LLM::ToolResult.new(id: ev.payload[:id], content: ev.payload[:content],
                            error: ev.payload[:error])
      ])
    end
  end
end

#fetch(id) ⇒ Object



34
# File 'lib/terret/sessions.rb', line 34

def fetch(id) = @store.fetch(id)

#fork(source_id, boundary: nil, child_id: SecureRandom.hex(6)) ⇒ Object



81
82
83
84
85
86
87
88
89
# File 'lib/terret/sessions.rb', line 81

def fork(source_id, boundary: nil, child_id: SecureRandom.hex(6))
  src = fetch(source_id)
  child = Session.new(id: child_id, events: [], parent_id: source_id)
  @store[child_id] = child
  events = boundary ? src.events.take(boundary) : src.events.dup
  events.each { |ev| child.events << ev.with(session_id: child_id) }
  append(child_id, "session/forked", { from: source_id, boundary: boundary })
  child
end

#start(ctx) ⇒ Object



21
22
23
24
25
# File 'lib/terret/sessions.rb', line 21

def start(ctx)
  @ctx = ctx
  @store = {}
  @dir = config[:jsonl_dir] # optional persistence
end