Class: Terret::Compactor

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

Overview

ctx — turns a long history into a short one without breaking "model-visible means logged" (§2.5): the summary is itself a durable event, and derive_messages projects it in place of everything at or before its boundary. The §12 contract: upto_seq is ALWAYS the seq immediately preceding the compaction event — the projection prepends the summary, so any gap would interleave it among events that predate it. The boundary is computed at append time, after the summarizer returns: only projection-invisible events can land during summarization (the agent is still mid-turn, so nothing model-visible can interleave).

Summary GENERATION is a seam: ctx (sole provider, like the session store). A summarizer may decline by returning nil/empty — compaction is an optimization, so a decline warns and the next overweight turn retries. A summarizer that raises inside the trigger is isolated by emit dispatch; a manual compact! raises through.

Constant Summary collapse

MODEL_VISIBLE =

Everything derive_messages projects. Anything else may land under a boundary without being summarized; these may not.

%w[user/message context/injected assistant/message
tool/result session/compacted].freeze

Instance Method Summary collapse

Instance Method Details

#compact!(session_id) ⇒ Object

Summarize the whole projected history and append the boundary event. Returns the appended SessionEvent, or nil when the summarizer declined.

Raises:

  • (ArgumentError)


48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/terret/compactor.rb', line 48

def compact!(session_id)
  sessions = @ctx[:sessions]
  history = sessions.derive_messages(session_id)
  raise ArgumentError, "nothing to compact in #{session_id}" if history.empty?

  base_seq = sessions.fetch(session_id).events.last.seq
  summary = @ctx[:summarizer].summarize(history)
  unless summary.is_a?(String) && !summary.strip.empty?
    warn "terret: compaction skipped for #{session_id}: summarizer declined"
    return nil
  end

  # Summarizing is a round trip; the boundary covers the whole prefix, so
  # model-visible history arriving while it ran would be swept under a
  # summary that never read it. Decline instead — the next overweight turn
  # retries, and nothing is lost in the meantime.
  raced = sessions.fetch(session_id).events
                  .count { |e| e.seq > base_seq && MODEL_VISIBLE.include?(e.type) }
  unless raced.zero?
    warn "terret: compaction skipped for #{session_id}: " \
         "#{raced} model-visible event(s) landed while summarizing"
    return nil
  end

  sessions.append(session_id, "session/compacted",
                  { upto_seq: sessions.fetch(session_id).events.last.seq,
                    summary: summary })
end

#reconfigure(config) ⇒ Object



37
38
39
# File 'lib/terret/compactor.rb', line 37

def reconfigure(config)
  @budget = config[:budget]
end

#start(ctx) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
# File 'lib/terret/compactor.rb', line 25

def start(ctx)
  @ctx = ctx
  @budget = config[:budget]
  # Always registered, gated at fire time: a hot-set budget (reconfigure)
  # arms the trigger without a remount.
  ctx.on("session/event") do |ev|
    next unless @budget && ev.type == "turn/end"

    compact!(ev.session_id) if overweight?(ev.session_id)
  end
end