Skip to content
Kward Search API index

Class: Kward::PanServer

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

Overview

Local HTTP server for the mobile-friendly Pan web UI.

Constant Summary collapse

DEFAULT_HOST =
"127.0.0.1"
DEFAULT_PORT =
8765
MAX_REQUEST_BODY_BYTES =
64 * 1024
ROUTING_PROBE_ADDRESS =
"8.8.8.8"
ROUTING_PROBE_PORT =
80

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client:, working_directory:, config: ConfigFiles.read_config, config_dir: ConfigFiles.config_dir, output: $stderr, udp_socket_class: UDPSocket) ⇒ PanServer

Returns a new instance of PanServer.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/kward/pan/server.rb', line 35

def initialize(client:, working_directory:, config: ConfigFiles.read_config, config_dir: ConfigFiles.config_dir, output: $stderr, udp_socket_class: UDPSocket)
  @client = client
  @output = output
  @udp_socket_class = udp_socket_class
  @workspace = WorkspaceFactory.build(
    root: working_directory,
    guardrails: ConfigFiles.workspace_guardrails_enabled?(config),
    config: config
  )
  @full_config = config
  @config = pan_config(config)
  @host = @config.fetch("host", DEFAULT_HOST).to_s
  @port = positive_port(@config.fetch("port", DEFAULT_PORT))
  @username = @config["username"].to_s
  environment_password = ENV["KWARD_PAN_PASSWORD"].to_s
  @password = environment_password.empty? ? @config["password"].to_s : environment_password
  if @username.empty? || @password.empty?
    raise "Pan mode requires pan_mode.username and either pan_mode.password or KWARD_PAN_PASSWORD"
  end

  @session_store = SessionStore.new(config_dir: config_dir, cwd: @workspace.root.to_s)
  @session = @session_store.create(
    provider: (@client.current_provider if @client.respond_to?(:current_provider)),
    model: (@client.current_model if @client.respond_to?(:current_model)),
    reasoning_effort: (@client.current_reasoning_effort if @client.respond_to?(:current_reasoning_effort))
  )
  @conversation = new_conversation
  @session.attach(@conversation)
  @agent = build_agent
  @prompt_queue = Queue.new
  @subscribers = []
  @subscribers_mutex = Mutex.new
  @worker_started = false
  @active = false
  @pending_turns = 0
  @state_mutex = Mutex.new
  @session_mutex = Mutex.new
end

Instance Attribute Details

#host ⇒ Object (readonly)

Returns the value of attribute host.



74
75
76
# File 'lib/kward/pan/server.rb', line 74

def host
  @host
end

#port ⇒ Object (readonly)

Returns the value of attribute port.



74
75
76
# File 'lib/kward/pan/server.rb', line 74

def port
  @port
end

#session ⇒ Object (readonly)

Returns the value of attribute session.



74
75
76
# File 'lib/kward/pan/server.rb', line 74

def session
  @session
end

#workspace ⇒ Object (readonly)

Returns the value of attribute workspace.



74
75
76
# File 'lib/kward/pan/server.rb', line 74

def workspace
  @workspace
end

Instance Method Details

#enqueue_prompt(prompt) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/kward/pan/server.rb', line 103

def enqueue_prompt(prompt)
  text = prompt.to_s
  return { ok: false, error: "Prompt is required" } if text.strip.empty?

  @session_mutex.synchronize do
    queued_at = Time.now.utc.iso8601(3)
    @state_mutex.synchronize { @pending_turns += 1 }
    @prompt_queue << { prompt: text, queued_at: queued_at }
  end
  broadcast("queue", queue_payload)
  { ok: true, queued: @prompt_queue.size, active: active? }
end

#run ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/kward/pan/server.rb', line 76

def run
  warn_if_exposed
  start_worker
  @server = TCPServer.new(@host, @port)
  actual_port = @server.addr[1]
  @output.puts "Kward pan mode listening on http://#{display_host}:#{actual_port}"
  @output.puts "Workspace: #{@workspace.root}"
  @output.puts "Session: #{@session.path}"

  loop do
    socket = @server.accept
    Thread.new(socket) { |client_socket| handle_client(client_socket) }
  end
rescue Interrupt
  @output.puts "\nPan mode stopped."
ensure
  stop
end

#stop ⇒ Object



95
96
97
98
99
100
101
# File 'lib/kward/pan/server.rb', line 95

def stop
  @server&.close unless @server&.closed?
rescue IOError
  nil
ensure
  @plugin_registry&.shutdown!
end

#transcript_items ⇒ Object



116
117
118
119
120
121
122
123
# File 'lib/kward/pan/server.rb', line 116

def transcript_items
  RPC::TranscriptNormalizer.new(@conversation.messages).normalize.flat_map do |message|
    items = pan_transcript_items(message)
    text = turn_summary_text(message[:turnSummary])
    items << { role: "turn-stats", label: "Turn summary", text: text } unless text.empty?
    items
  end
end