Kward

Class: Kward::Conversation

Inherits:
Object
  • Object
show all
Defined in:
lib/kward/conversation.rb

Overview

Mutable transcript and runtime context for one agent session.

Conversation owns message ordering, system prompt refresh, read-before-write state, memory prompt context, and persistence hooks. It intentionally stores plain hashes because provider payload builders, session JSONL files, and RPC normalizers all share the same transcript shape. Use MessageAccess when reading messages so symbol/string key and legacy field compatibility stays in one place.

Frontends should not mutate messages directly after attaching a SessionStore::Session; use append/compact helpers so persistence callbacks run and session trees stay consistent.

Constant Summary collapse

DEFAULT_SYSTEM_MESSAGE =
Object.new.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(system_message: DEFAULT_SYSTEM_MESSAGE, messages: [], read_paths: [], on_append: nil, on_compact: nil, on_tool_execution: nil, on_runtime_update: nil, workspace_root: Dir.pwd, compaction_system_message: DEFAULT_SYSTEM_MESSAGE, provider: nil, model: nil, reasoning_effort: nil, memory_context: nil, session_memories: [], last_memory_retrieval: nil, plugin_registry: nil) ⇒ Conversation

Returns a new instance of Conversation.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/kward/conversation.rb', line 61

def initialize(system_message: DEFAULT_SYSTEM_MESSAGE, messages: [], read_paths: [], on_append: nil, on_compact: nil, on_tool_execution: nil, on_runtime_update: nil, workspace_root: Dir.pwd, compaction_system_message: DEFAULT_SYSTEM_MESSAGE, provider: nil, model: nil, reasoning_effort: nil, memory_context: nil, session_memories: [], last_memory_retrieval: nil, plugin_registry: nil)
  @workspace_root = ConfigFiles.canonical_workspace_root(workspace_root)
  @provider = provider
  @model = model
  @reasoning_effort = reasoning_effort
  @plugin_registry = plugin_registry
  @messages = []
  restored_system_message, transcript_messages = split_system_message(messages)
  if system_message.equal?(DEFAULT_SYSTEM_MESSAGE)
    if restored_system_message
      system_message = restored_system_message
    else
      @last_plugin_prompt_context = plugin_prompt_context
      system_message = Prompts.system_message(workspace_root: @workspace_root, model: @model, reasoning_effort: @reasoning_effort, memory_context: memory_context, plugin_context: @last_plugin_prompt_context)
    end
  end
  @system_message = system_message
  @system_message_enabled = !@system_message.nil?
  if compaction_system_message.equal?(DEFAULT_SYSTEM_MESSAGE)
    compaction_system_message = @system_message_enabled ? Prompts.system_message(workspace_root: @workspace_root, include_workspace_personality: false, model: @model, reasoning_effort: @reasoning_effort) : nil
  end
  @compaction_system_message = compaction_system_message
  @workspace_agents_mtime = workspace_agents_mtime
  @last_entry_compaction = false
  @memory_context = memory_context
  @session_memories = Array(session_memories)
  @last_memory_retrieval = last_memory_retrieval
  @messages.concat(transcript_messages)
  @read_paths = Set.new(read_paths)
  @on_append = on_append
  @on_compact = on_compact
  @on_tool_execution = on_tool_execution
  @on_runtime_update = on_runtime_update
  @on_system_message_change = nil
end

Instance Attribute Details

#compaction_system_messageHash? (readonly)

Returns system prompt used when summarizing old context.

Returns:

  • (Hash, nil)

    system prompt used when summarizing old context



33
34
35
# File 'lib/kward/conversation.rb', line 33

def compaction_system_message
  @compaction_system_message
end

#last_memory_retrievalHash?

Returns metadata for the last memory retrieval attached to the session.

Returns:

  • (Hash, nil)

    metadata for the last memory retrieval attached to the session



55
56
57
# File 'lib/kward/conversation.rb', line 55

def last_memory_retrieval
  @last_memory_retrieval
end

#last_plugin_prompt_contextString? (readonly)

Returns plugin prompt context used in the current system prompt.

Returns:

  • (String, nil)

    plugin prompt context used in the current system prompt



59
60
61
# File 'lib/kward/conversation.rb', line 59

def last_plugin_prompt_context
  @last_plugin_prompt_context
end

#memory_contextString?

Returns memory prompt context injected into refreshed system messages.

Returns:

  • (String, nil)

    memory prompt context injected into refreshed system messages



53
54
55
# File 'lib/kward/conversation.rb', line 53

def memory_context
  @memory_context
end

#messagesArray<Hash> (readonly)

Returns ordered durable transcript entries, excluding runtime system prompt state.

Returns:

  • (Array<Hash>)

    ordered durable transcript entries, excluding runtime system prompt state



25
26
27
# File 'lib/kward/conversation.rb', line 25

def messages
  @messages
end

#modelString? (readonly)

Returns model id captured for session/runtime prompts.

Returns:

  • (String, nil)

    model id captured for session/runtime prompts



37
38
39
# File 'lib/kward/conversation.rb', line 37

def model
  @model
end

#on_appendProc?

Returns persistence callback invoked after appending a message.

Returns:

  • (Proc, nil)

    persistence callback invoked after appending a message



43
44
45
# File 'lib/kward/conversation.rb', line 43

def on_append
  @on_append
end

#on_compactProc?

Returns persistence callback invoked after compaction replaces history.

Returns:

  • (Proc, nil)

    persistence callback invoked after compaction replaces history



45
46
47
# File 'lib/kward/conversation.rb', line 45

def on_compact
  @on_compact
end

#on_runtime_updateProc?

Returns callback invoked when runtime metadata should be persisted.

Returns:

  • (Proc, nil)

    callback invoked when runtime metadata should be persisted



49
50
51
# File 'lib/kward/conversation.rb', line 49

def on_runtime_update
  @on_runtime_update
end

#on_system_message_changeProc?

Returns callback invoked when the system prompt runtime state changes.

Returns:

  • (Proc, nil)

    callback invoked when the system prompt runtime state changes



51
52
53
# File 'lib/kward/conversation.rb', line 51

def on_system_message_change
  @on_system_message_change
end

#on_tool_executionProc?

Returns callback invoked when a tool execution record should be persisted.

Returns:

  • (Proc, nil)

    callback invoked when a tool execution record should be persisted



47
48
49
# File 'lib/kward/conversation.rb', line 47

def on_tool_execution
  @on_tool_execution
end

#plugin_registryPluginRegistry?

Returns registry used to collect plugin prompt context.

Returns:

  • (PluginRegistry, nil)

    registry used to collect plugin prompt context



57
58
59
# File 'lib/kward/conversation.rb', line 57

def plugin_registry
  @plugin_registry
end

#providerString? (readonly)

Returns provider captured for session/runtime prompts.

Returns:

  • (String, nil)

    provider captured for session/runtime prompts



35
36
37
# File 'lib/kward/conversation.rb', line 35

def provider
  @provider
end

#read_pathsSet<String> (readonly)

Returns resolved paths read by file tools during the active context.

Returns:

  • (Set<String>)

    resolved paths read by file tools during the active context



29
30
31
# File 'lib/kward/conversation.rb', line 29

def read_paths
  @read_paths
end

#reasoning_effortString? (readonly)

Returns reasoning effort captured for session/runtime prompts.

Returns:

  • (String, nil)

    reasoning effort captured for session/runtime prompts



39
40
41
# File 'lib/kward/conversation.rb', line 39

def reasoning_effort
  @reasoning_effort
end

#session_memoriesArray<Hash> (readonly)

Returns memories scoped to this conversation session.

Returns:

  • (Array<Hash>)

    memories scoped to this conversation session



41
42
43
# File 'lib/kward/conversation.rb', line 41

def session_memories
  @session_memories
end

#system_messageHash? (readonly)

Returns current system prompt included when building provider request context.

Returns:

  • (Hash, nil)

    current system prompt included when building provider request context



27
28
29
# File 'lib/kward/conversation.rb', line 27

def system_message
  @system_message
end

#workspace_rootString (readonly)

Returns canonical workspace root used for prompts and file guardrails.

Returns:

  • (String)

    canonical workspace root used for prompts and file guardrails



31
32
33
# File 'lib/kward/conversation.rb', line 31

def workspace_root
  @workspace_root
end

Instance Method Details

#append_assistant(message) ⇒ Object



108
109
110
111
# File 'lib/kward/conversation.rb', line 108

def append_assistant(message)
  message = { role: "assistant", content: message } if message.is_a?(String)
  append_message(message)
end

#append_tool(tool_call_id:, name:, content:) ⇒ Object



113
114
115
116
117
118
119
120
# File 'lib/kward/conversation.rb', line 113

def append_tool(tool_call_id:, name:, content:)
  append_message({
    role: "tool",
    tool_call_id: tool_call_id,
    name: name,
    content: content
  })
end

#append_tool_execution(tool_call:, content:) ⇒ Object



122
123
124
# File 'lib/kward/conversation.rb', line 122

def append_tool_execution(tool_call:, content:)
  @on_tool_execution&.call(tool_call, content)
end

#append_user(content, display_content: nil) ⇒ Object

Appends a user message and normalizes image attachment syntax.

display_content is transcript/UI text for cases where the model input is expanded, decorated, or contains encoded attachment content.



101
102
103
104
105
106
# File 'lib/kward/conversation.rb', line 101

def append_user(content, display_content: nil)
  content = ImageAttachments.content_from_text(content) unless content.is_a?(Array)
  message = { role: "user", content: content }
  message[:display_content] = display_content.to_s unless display_content.nil?
  append_message(message)
end

#compact!(summary, compaction_summary: false, first_kept_entry_id: nil, tokens_before: nil, from_hook: false, details: {}, keep_messages: []) ⇒ Object

Replaces most transcript entries with a compaction summary and optional recent messages to keep.

Compaction clears read-before-write state because file contents observed before the summary may no longer be represented exactly in the active context. Callers that need file mutation after compaction should read files again through the normal tools.



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/kward/conversation.rb', line 182

def compact!(summary, compaction_summary: false, first_kept_entry_id: nil, tokens_before: nil, from_hook: false, details: {}, keep_messages: [])
  message = if compaction_summary
              { role: "compactionSummary", summary: summary.to_s }
            else
              { role: "assistant", content: summary.to_s }
            end
  if compaction_summary
    message[:first_kept_entry_id] = first_kept_entry_id if first_kept_entry_id
    message[:tokens_before] = tokens_before if tokens_before
    message[:from_hook] = from_hook
    message[:details] = details || {}
  end
  @messages = []
  @messages << message
  @messages.concat(Array(keep_messages))
  @read_paths.clear
  @last_entry_compaction = true
  @on_compact&.call(message)
  message
end

#context_messagesArray<Hash>

Returns provider request context: current system prompt plus durable transcript.

Returns:

  • (Array<Hash>)

    provider request context: current system prompt plus durable transcript



127
128
129
# File 'lib/kward/conversation.rb', line 127

def context_messages
  @system_message ? [@system_message] + @messages : @messages.dup
end

#last_entry_compaction?Boolean

Returns:

  • (Boolean)


203
204
205
# File 'lib/kward/conversation.rb', line 203

def last_entry_compaction?
  @last_entry_compaction
end

#last_file_change_resultObject



211
212
213
214
215
# File 'lib/kward/conversation.rb', line 211

def last_file_change_result
  @messages.select do |message|
    MessageAccess.role(message) == "tool" && ["write_file", "edit_file"].include?(MessageAccess.name(message))
  end.last
end

#mark_last_entry_compaction!Object



207
208
209
# File 'lib/kward/conversation.rb', line 207

def mark_last_entry_compaction!
  @last_entry_compaction = true
end

#mark_read(path) ⇒ Object



164
165
166
# File 'lib/kward/conversation.rb', line 164

def mark_read(path)
  @read_paths << path
end

#persist_runtime_context!Object



156
157
158
# File 'lib/kward/conversation.rb', line 156

def persist_runtime_context!
  @on_runtime_update&.call(provider: @provider, model: @model, reasoning_effort: @reasoning_effort)
end

#plugin_prompt_contextObject



168
169
170
171
172
173
# File 'lib/kward/conversation.rb', line 168

def plugin_prompt_context
  return nil unless plugin_registry

  context = PluginRegistry::Context.new(conversation: self, workspace_root: @workspace_root)
  plugin_registry.prompt_context(context)
end

#refresh_system_message!Object

Rebuilds the system message from current config, memory, plugins, and workspace AGENTS.md state.

Conversations created with system_message: nil keep system prompts disabled; this preserves tests, compaction summaries, and imported transcripts that intentionally do not include runtime instructions.



137
138
139
140
141
142
143
144
145
146
147
# File 'lib/kward/conversation.rb', line 137

def refresh_system_message!
  return nil unless @system_message_enabled

  @last_plugin_prompt_context = plugin_prompt_context
  replacement = Prompts.system_message(workspace_root: @workspace_root, model: @model, reasoning_effort: @reasoning_effort, memory_context: @memory_context, plugin_context: @last_plugin_prompt_context)
  @system_message = replacement
  @on_system_message_change&.call(replacement)
  @compaction_system_message = Prompts.system_message(workspace_root: @workspace_root, include_workspace_personality: false, model: @model, reasoning_effort: @reasoning_effort)
  @workspace_agents_mtime = workspace_agents_mtime
  replacement
end

#refresh_system_message_if_workspace_agents_changed!Object



160
161
162
# File 'lib/kward/conversation.rb', line 160

def refresh_system_message_if_workspace_agents_changed!
  refresh_system_message! if @system_message_enabled && workspace_agents_mtime != @workspace_agents_mtime
end

#update_runtime_context!(provider: nil, model:, reasoning_effort:) ⇒ Object



149
150
151
152
153
154
# File 'lib/kward/conversation.rb', line 149

def update_runtime_context!(provider: nil, model:, reasoning_effort:)
  @provider = provider unless provider.to_s.empty?
  @model = model
  @reasoning_effort = reasoning_effort
  refresh_system_message!
end