Skip to content
Kward Search API index

Class: Kward::PluginChatRuntime

Inherits:
Object
  • Object
show all
Defined in:
lib/kward/plugin_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.



50
51
52
53
54
55
56
57
58
# File 'lib/kward/plugin_chat_runtime.rb', line 50

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
end

Instance Method Details

#cancel_turn(turn_id:) ⇒ Object



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

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



85
86
87
# File 'lib/kward/plugin_chat_runtime.rb', line 85

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

#input_with_attachments(input, attachments) ⇒ 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.



152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/kward/plugin_chat_runtime.rb', line 152

def input_with_attachments(input, attachments)
  attachments = Array(attachments)
  return input.to_s if attachments.empty?

  [{ 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



133
134
135
136
137
138
# File 'lib/kward/plugin_chat_runtime.rb', line 133

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)


78
79
80
81
82
83
# File 'lib/kward/plugin_chat_runtime.rb', line 78

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, scope_key: scope_key, descriptor: descriptor, workspace_root: workspace_root)
end

#shutdownObject



140
141
142
143
144
145
146
147
# File 'lib/kward/plugin_chat_runtime.rb', line 140

def shutdown
  chats = @mutex.synchronize { @chats.values.dup }
  chats.each do |chat|
    chat.queue << WORKER_STOP if chat.worker&.alive?
    chat.worker&.join(0.2)
  end
  nil
end

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



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

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)


71
72
73
74
75
76
# File 'lib/kward/plugin_chat_runtime.rb', line 71

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



60
61
62
63
64
65
66
67
68
69
# File 'lib/kward/plugin_chat_runtime.rb', line 60

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



126
127
128
129
130
131
# File 'lib/kward/plugin_chat_runtime.rb', line 126

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



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

def turn_status(turn_id:)
  fetch_turn(turn_id)
end