Class: Yorishiro::Conversation
- Inherits:
-
Object
- Object
- Yorishiro::Conversation
- Defined in:
- lib/yorishiro/conversation.rb
Constant Summary collapse
- CHARS_PER_TOKEN =
Rough heuristic used to estimate token counts without a real tokenizer. English/code averages ~4 characters per token.
4- ELIDED_TOOL_RESULT =
"[Old tool result removed to free context. Re-run the tool if the output is needed again.]"
Instance Attribute Summary collapse
-
#messages ⇒ Object
readonly
Returns the value of attribute messages.
Instance Method Summary collapse
- #add_message(role, content, tool_calls: nil, tool_call_id: nil) ⇒ Object
- #add_tool_result(tool_call_id:, content:) ⇒ Object
- #clear ⇒ Object
-
#compact!(keep_recent_rounds: 2) ⇒ Object
Replace the oldest rounds with a single summary while keeping the most recent
keep_recent_roundsrounds verbatim. -
#elide_old_tool_results!(max_tokens:, keep_recent: 2) ⇒ Object
Replace the contents of the oldest tool results with a short placeholder until the conversation fits within
max_tokens, keeping the most recentkeep_recenttool results verbatim. -
#estimated_tokens ⇒ Object
Rough estimate of the prompt size in tokens (system prompt + all messages).
-
#initialize(system_prompt: nil) ⇒ Conversation
constructor
A new instance of Conversation.
- #last_role ⇒ Object
- #length ⇒ Object
-
#restore_messages!(raw_messages) ⇒ Object
Replace the messages with ones loaded from a saved session (string-keyed hashes as produced by #serializable_messages).
-
#serializable_messages ⇒ Object
JSON-safe deep copy of the messages (string keys) for session persistence.
- #to_api_messages ⇒ Object
-
#trim_to_budget!(max_tokens:) ⇒ Object
Drop the oldest conversation rounds until the estimated size fits within
max_tokens.
Constructor Details
#initialize(system_prompt: nil) ⇒ Conversation
Returns a new instance of Conversation.
11 12 13 14 |
# File 'lib/yorishiro/conversation.rb', line 11 def initialize(system_prompt: nil) @messages = [] @system_prompt = system_prompt end |
Instance Attribute Details
#messages ⇒ Object (readonly)
Returns the value of attribute messages.
9 10 11 |
# File 'lib/yorishiro/conversation.rb', line 9 def @messages end |
Instance Method Details
#add_message(role, content, tool_calls: nil, tool_call_id: nil) ⇒ Object
16 17 18 19 20 21 22 23 24 |
# File 'lib/yorishiro/conversation.rb', line 16 def (role, content, tool_calls: nil, tool_call_id: nil) validate_role!(role) @messages << { role: role, content: content, tool_calls: tool_calls, tool_call_id: tool_call_id }.compact end |
#add_tool_result(tool_call_id:, content:) ⇒ Object
26 27 28 29 30 31 32 |
# File 'lib/yorishiro/conversation.rb', line 26 def add_tool_result(tool_call_id:, content:) @messages << { role: :tool, content: content, tool_call_id: tool_call_id } end |
#clear ⇒ Object
47 48 49 |
# File 'lib/yorishiro/conversation.rb', line 47 def clear @messages.clear end |
#compact!(keep_recent_rounds: 2) ⇒ Object
Replace the oldest rounds with a single summary while keeping the most
recent keep_recent_rounds rounds verbatim. The block receives the old
messages and must return the summary text; rounds are handled whole so a
tool call is never split from its result, and the summary is inserted as a
leading :user message (keeping the user-first ordering providers expect).
Returns the number of messages that were compacted away (0 if there was
nothing old enough to compact or the summary came back empty).
98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/yorishiro/conversation.rb', line 98 def compact!(keep_recent_rounds: 2) starts = return 0 if starts.length <= keep_recent_rounds cut = starts[starts.length - keep_recent_rounds] old = @messages[0...cut] return 0 if old.empty? summary = yield(old) return 0 if summary.nil? || summary.strip.empty? @messages = [(summary)] + (@messages[cut..] || []) old.length end |
#elide_old_tool_results!(max_tokens:, keep_recent: 2) ⇒ Object
Replace the contents of the oldest tool results with a short
placeholder until the conversation fits within max_tokens, keeping
the most recent keep_recent tool results verbatim. Unlike
trim_to_budget!, this frees space inside a single round — a long tool
loop after one user message — where whole-round trimming cannot drop
anything. Message structure is preserved, so an assistant tool_call is
never left without its result. Returns the number of results elided.
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/yorishiro/conversation.rb', line 122 def elide_old_tool_results!(max_tokens:, keep_recent: 2) tool_indices = @messages.each_index.select { |i| @messages[i][:role] == :tool } candidates = keep_recent.positive? ? tool_indices[0...-keep_recent] : tool_indices elided = 0 (candidates || []).each do |index| break if estimated_tokens <= max_tokens next if @messages[index][:content] == ELIDED_TOOL_RESULT @messages[index][:content] = ELIDED_TOOL_RESULT elided += 1 end elided end |
#estimated_tokens ⇒ Object
Rough estimate of the prompt size in tokens (system prompt + all messages).
85 86 87 88 89 |
# File 'lib/yorishiro/conversation.rb', line 85 def estimated_tokens total = @system_prompt.to_s.length total += @messages.sum { |msg| (msg) } (total.to_f / CHARS_PER_TOKEN).ceil end |
#last_role ⇒ Object
76 77 78 |
# File 'lib/yorishiro/conversation.rb', line 76 def last_role @messages.last&.fetch(:role, nil) end |
#length ⇒ Object
80 81 82 |
# File 'lib/yorishiro/conversation.rb', line 80 def length @messages.length end |
#restore_messages!(raw_messages) ⇒ Object
Replace the messages with ones loaded from a saved session (string-keyed hashes as produced by #serializable_messages). Tool-call arguments keep their string keys: providers pass them through as-is and the CLI symbolizes at execution time, matching the live message flow.
63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/yorishiro/conversation.rb', line 63 def () @messages = .map do |msg| role = msg["role"].to_sym validate_role!(role) { role: role, content: msg["content"], tool_calls: msg["tool_calls"]&.map { |tc| { id: tc["id"], name: tc["name"], arguments: tc["arguments"] } }, tool_call_id: msg["tool_call_id"] }.compact end end |
#serializable_messages ⇒ Object
JSON-safe deep copy of the messages (string keys) for session persistence. The system prompt is intentionally excluded — a resumed session uses whatever the current configuration provides.
54 55 56 |
# File 'lib/yorishiro/conversation.rb', line 54 def JSON.parse(JSON.generate(@messages)) end |
#to_api_messages ⇒ Object
34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/yorishiro/conversation.rb', line 34 def msgs = @messages.map do |msg| converted = { role: msg[:role].to_s, content: msg[:content] } converted[:tool_calls] = msg[:tool_calls] if msg[:tool_calls] converted[:tool_call_id] = msg[:tool_call_id] if msg[:tool_call_id] converted end msgs.unshift({ role: "system", content: @system_prompt }) if @system_prompt msgs end |
#trim_to_budget!(max_tokens:) ⇒ Object
Drop the oldest conversation rounds until the estimated size fits within
max_tokens. A "round" starts at a :user message and includes the
assistant reply and any tool results that follow, so whole rounds are
removed together — never splitting an assistant tool_call from its tool
result. The system prompt and the most recent round are always kept.
Returns the number of messages removed.
144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/yorishiro/conversation.rb', line 144 def trim_to_budget!(max_tokens:) removed_count = 0 while estimated_tokens > max_tokens round_starts = break if round_starts.length <= 1 # keep at least the latest round removed = @messages.slice!(0, round_starts[1]) removed_count += removed.length end removed_count end |