Skip to content
Kward Search API index

Class: Kward::PluginChatRuntime

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

Overview

Coordinates plugin chat drivers without depending on RPC or the terminal UI. Frontends use this runtime for queuing, cancellation, event replay, and transcript access while the plugin remains responsible for its own storage, model settings, and domain behavior.

Defined Under Namespace

Classes: Chat, Turn

Constant Summary collapse

EVENT_LIMIT =
1_000
WORKER_STOP =
Object.new.freeze

Instance Method Summary collapse

Constructor Details

#initialize(client:, plugin_registry_provider: nil, message_normalizer: nil) ⇒ PluginChatRuntime

Returns a new instance of PluginChatRuntime.



51
52
53
54
55
56
57
58
59
60
# File 'lib/kward/plugins/chat_runtime.rb', line 51

def initialize(client:, plugin_registry_provider: nil, message_normalizer: nil)
  @client = client
  @plugin_registry_provider = plugin_registry_provider
  @message_normalizer = message_normalizer
  @chats = {}
  @turns = {}
  @event_listeners = []
  @mutex = Mutex.new
  @shutdown = false
end

Instance Method Details

#cancel_turn(turn_id:) ⇒ Object



113
114
115
116
117
118
119
120
121
122
# File 'lib/kward/plugins/chat_runtime.rb', line 113

def cancel_turn(turn_id:)
  turn = fetch_turn(turn_id)
  queued = turn.mutex.synchronize do
    turn.cancellation.cancel!
    turn.status == "queued"
  end
  emit_event(turn, "turnCancelRequested", {})
  finish_turn(turn, "canceled") if queued
  turn
end

#chat(chat_id) ⇒ Object



87
88
89
# File 'lib/kward/plugins/chat_runtime.rb', line 87

def chat(chat_id)
  @mutex.synchronize { @chats[chat_id.to_s] }
end

#input_with_attachments(input, attachments, capabilities: PluginChatCapabilities.legacy) ⇒ Object

Converts normalized image attachment hashes into the input shape accepted by plugin chat drivers. RPC and transport frontends can normalize their own boundary formats before calling this helper.



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/kward/plugins/chat_runtime.rb', line 162

def input_with_attachments(input, attachments, capabilities: PluginChatCapabilities.legacy)
  attachments = Array(attachments)
  return input.to_s if attachments.empty?
  if capabilities.declared? && !capabilities.allows_attachment?(:image)
    raise ArgumentError, "plugin chat does not allow image attachments"
  end

  [{ type: "text", text: input.to_s }] + attachments.map do |attachment|
    {
      type: "image",
      media_type: attachment.fetch(:mimeType) { attachment.fetch("mimeType") },
      data: attachment.fetch(:data) { attachment.fetch("data") },
      alt: attachment[:alt] || attachment["alt"]
    }.compact
  end
end

#list_turns(chat_id: nil, active: false) ⇒ Object



135
136
137
138
139
140
# File 'lib/kward/plugins/chat_runtime.rb', line 135

def list_turns(chat_id: nil, active: false)
  turns = @mutex.synchronize { @turns.values.dup }
  turns.select! { |turn| turn.chat_id == chat_id.to_s } if chat_id
  turns.select! { |turn| %w[queued running].include?(turn.status) } if active
  turns
end

#open(type_id:, surface: :rpc, scope_key: nil, descriptor: {}, workspace_root: Dir.pwd) ⇒ Object

Raises:

  • (ArgumentError)


80
81
82
83
84
85
# File 'lib/kward/plugins/chat_runtime.rb', line 80

def open(type_id:, surface: :rpc, scope_key: nil, descriptor: {}, workspace_root: Dir.pwd)
  type = supported_types(surface: surface).find { |entry| entry.id == type_id.to_s }
  raise ArgumentError, "Unknown #{surface} plugin chat: #{type_id}" unless type

  chat_for(type, surface: surface, scope_key: scope_key, descriptor: descriptor, workspace_root: workspace_root)
end

#shutdown ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/kward/plugins/chat_runtime.rb', line 142

def shutdown
  chats = @mutex.synchronize do
    return nil if @shutdown

    @shutdown = true
    @chats.values.dup
  end
  @mutex.synchronize { @turns.values.dup }.each { |turn| turn.cancellation.cancel! }
  chats.each do |chat|
    chat.queue << WORKER_STOP if chat.worker&.alive?
    chat.worker&.join(0.2)
    close_chat(chat)
  end
  @plugin_registry&.shutdown! unless @plugin_registry_provider
  nil
end

#start_turn(chat_id:, input:, display_input: nil, context: nil) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/kward/plugins/chat_runtime.rb', line 91

def start_turn(chat_id:, input:, display_input: nil, context: nil)
  chat = fetch_chat(chat_id)
  turn = Turn.new(
    id: SecureRandom.uuid,
    chat_id: chat.id,
    input: input,
    display_input: display_input.nil? ? input.to_s : display_input.to_s,
    context: context || {},
    status: "queued",
    cancellation: Cancellation.new,
    created_at: Time.now.utc.iso8601(3),
    events: [],
    next_sequence: 1,
    mutex: Mutex.new
  )
  @mutex.synchronize { @turns[turn.id] = turn }
  chat.queue << turn.id
  ensure_worker(chat)
  emit_event(turn, "turnQueued", { status: "queued" })
  turn
end

#subscribe_events(&block) ⇒ Object

Raises:

  • (ArgumentError)


73
74
75
76
77
78
# File 'lib/kward/plugins/chat_runtime.rb', line 73

def subscribe_events(&block)
  raise ArgumentError, "plugin chat event subscription requires a block" unless block

  @mutex.synchronize { @event_listeners << block }
  block
end

#supported_types(surface: :rpc) ⇒ Object



62
63
64
65
66
67
68
69
70
71
# File 'lib/kward/plugins/chat_runtime.rb', line 62

def supported_types(surface: :rpc)
  case surface.to_sym
  when :rpc
    plugin_registry.tab_types.select(&:rpc)
  when :transport
    plugin_registry.transport_tab_types
  else
    raise ArgumentError, "Unknown plugin chat surface: #{surface}"
  end
end

#turn_events(turn_id:, after_sequence: 0) ⇒ Object



128
129
130
131
132
133
# File 'lib/kward/plugins/chat_runtime.rb', line 128

def turn_events(turn_id:, after_sequence: 0)
  turn = fetch_turn(turn_id)
  turn.mutex.synchronize do
    turn.events.select { |event| event[:sequence].to_i > after_sequence.to_i }
  end
end

#turn_status(turn_id:) ⇒ Object



124
125
126
# File 'lib/kward/plugins/chat_runtime.rb', line 124

def turn_status(turn_id:)
  fetch_turn(turn_id)
end