Module: Brute::Compaction::Transcript

Defined in:
lib/brute/compaction/transcript.rb

Overview

The shapes a compaction strategy selects over.

A transcript is an Array of Brute::Message, but what may be given up is never a single message. An assistant's tool calls travel with the results that answer them, and a user's question with everything said before the next one -- a provider rejects the halves. So a strategy works in groups: a step is an assistant message and the tool results immediately after it, a turn is a real user message and everything up to the next.

A message a strategy produced carries its name at the head of its content, because Brute::Message has nowhere else to put it. That is what stops a later pass from summarising a summary.

Constant Summary collapse

MARK =
/\A\[compacted:([a-z_]+)\]/

Class Method Summary collapse

Class Method Details

.at(messages, indices) ⇒ Object



41
# File 'lib/brute/compaction/transcript.rb', line 41

def self.at(messages, indices) = indices.map { |index| messages[index] }

.line(message) ⇒ Object



52
# File 'lib/brute/compaction/transcript.rb', line 52

def self.line(message) = Brute::TokenCounter::Rendering.message(message)

.mark(strategy, text) ⇒ Object



24
# File 'lib/brute/compaction/transcript.rb', line 24

def self.mark(strategy, text) = "[compacted:#{strategy}] #{text}"

.marked?(message, strategy = nil) ⇒ Boolean

Returns:

  • (Boolean)


26
27
28
29
30
31
32
33
34
# File 'lib/brute/compaction/transcript.rb', line 26

def self.marked?(message, strategy = nil)
  name = message.content.to_s[MARK, 1]

  if strategy.nil?
    !name.nil?
  else
    name == strategy.to_s
  end
end

.render(messages) ⇒ Object

The transcript as plain text, for a summariser to read. Rendering it rather than replaying the messages keeps a half tool exchange -- a call whose result was left behind, a result whose call was -- off the wire, which is a shape providers refuse.

It is the same text the counters measure, so what a summariser is asked to shrink and what the trigger weighed are one thing.



50
# File 'lib/brute/compaction/transcript.rb', line 50

def self.render(messages) = Brute::TokenCounter::Rendering.conversation(messages)

.step_start(messages) ⇒ Object

Where the current task's steps begin: after its anchor, or after the instructions when the conversation has no anchor at all.



67
68
69
70
71
72
73
74
75
# File 'lib/brute/compaction/transcript.rb', line 67

def self.step_start(messages)
  task = task_index(messages)

  if task.nil?
    system_end(messages)
  else
    task + 1
  end
end

.steps(messages, from:) ⇒ Object

An assistant message and every tool result answering it, as index ranges.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/brute/compaction/transcript.rb', line 78

def self.steps(messages, from:)
  [].tap do |spans|
    index = from

    while index < messages.length
      if messages[index].role == :assistant
        finish = index + 1

        while finish < messages.length && messages[finish].role == :tool
          finish += 1
        end

        spans << (index...finish)
        index = finish
      else
        index += 1
      end
    end
  end
end

.system_end(messages) ⇒ Object

Where the leading system block ends. Nothing above this is ever given up.



55
56
57
# File 'lib/brute/compaction/transcript.rb', line 55

def self.system_end(messages)
  messages.index { |message| message.role != :system } || messages.length
end

.task_index(messages) ⇒ Object

The user message the current task hangs off, ignoring whatever an earlier compaction left behind.



61
62
63
# File 'lib/brute/compaction/transcript.rb', line 61

def self.task_index(messages)
  messages.rindex { |message| message.role == :user && !marked?(message) }
end

.tokens(messages) ⇒ Object

Roughly what a slice costs, for code holding messages rather than an env. A strategy weighs with the turn's own counter instead -- see Brute::Compaction.counter.



39
# File 'lib/brute/compaction/transcript.rb', line 39

def self.tokens(messages) = Brute::TokenCounter.default.count(messages)

.turns(messages, from:, to:) ⇒ Object

A real user message and everything up to the next one, as index ranges.



100
101
102
103
104
105
106
107
108
# File 'lib/brute/compaction/transcript.rb', line 100

def self.turns(messages, from:, to:)
  starts = (from...to).select do |index|
    messages[index].role == :user && !marked?(messages[index])
  end

  starts.each_with_index.map do |start, position|
    start...(starts[position + 1] || to)
  end
end