kward 0.79.0 → 0.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +6 -0
- data/CHANGELOG.md +58 -1
- data/Gemfile.lock +2 -2
- data/README.md +31 -38
- data/doc/agent-tools.md +9 -10
- data/doc/api.md +3 -3
- data/doc/authentication.md +79 -110
- data/doc/code-search.md +1 -1
- data/doc/composer.md +1 -1
- data/doc/configuration.md +69 -26
- data/doc/context-budgeting.md +6 -6
- data/doc/context-tools.md +2 -2
- data/doc/editor.md +7 -7
- data/doc/extensibility.md +7 -29
- data/doc/files.md +6 -6
- data/doc/getting-started.md +2 -2
- data/doc/git.md +8 -2
- data/doc/lifecycle-hooks.md +5 -4
- data/doc/local-models.md +2 -2
- data/doc/mcp.md +5 -5
- data/doc/permissions.md +13 -4
- data/doc/plugins.md +39 -4
- data/doc/prompt-templates.md +105 -0
- data/doc/providers.md +140 -0
- data/doc/releasing.md +10 -2
- data/doc/rpc.md +54 -31
- data/doc/sandboxing.md +22 -0
- data/doc/security.md +22 -7
- data/doc/session-management.md +2 -4
- data/doc/tabs.md +37 -0
- data/doc/telegram.md +138 -0
- data/doc/transports.md +209 -0
- data/doc/usage.md +11 -3
- data/doc/web-search.md +3 -3
- data/doc/workspace-tools.md +2 -2
- data/examples/plugins/stardate_footer.rb +10 -0
- data/examples/plugins/telegram/plugin.rb +39 -0
- data/examples/plugins/telegram/telegram_api.rb +136 -0
- data/examples/plugins/telegram/telegram_transport.rb +304 -0
- data/lib/kward/ansi.rb +1 -0
- data/lib/kward/auth/api_key_store.rb +99 -0
- data/lib/kward/cli/auth_commands.rb +72 -13
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/git.rb +31 -8
- data/lib/kward/cli/plugins.rb +1 -0
- data/lib/kward/cli/prompt_interface.rb +7 -0
- data/lib/kward/cli/rendering.rb +6 -2
- data/lib/kward/cli/runtime_helpers.rb +4 -2
- data/lib/kward/cli/sessions.rb +1 -1
- data/lib/kward/cli/settings.rb +97 -33
- data/lib/kward/cli/tabs.rb +102 -37
- data/lib/kward/cli/transports.rb +67 -0
- data/lib/kward/cli/worktrees.rb +360 -0
- data/lib/kward/cli.rb +20 -0
- data/lib/kward/config_files.rb +26 -3
- data/lib/kward/conversation.rb +13 -7
- data/lib/kward/git_worktree_manager.rb +291 -0
- data/lib/kward/model/azure_openai_config.rb +54 -0
- data/lib/kward/model/catalog.rb +113 -0
- data/lib/kward/model/client.rb +225 -20
- data/lib/kward/model/model_info.rb +49 -0
- data/lib/kward/model/payloads.rb +63 -4
- data/lib/kward/model/provider_catalog.rb +114 -0
- data/lib/kward/model/sources.rb +70 -0
- data/lib/kward/model/stream_parser.rb +83 -13
- data/lib/kward/permissions/policy.rb +18 -4
- data/lib/kward/plugin_chat_runtime.rb +374 -0
- data/lib/kward/plugin_registry.rb +80 -7
- data/lib/kward/private_file.rb +9 -3
- data/lib/kward/prompt_interface/approval_prompt.rb +2 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -5
- data/lib/kward/prompt_interface/editor/auto_indent.rb +46 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +18 -4
- data/lib/kward/prompt_interface/editor/controller.rb +7 -7
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
- data/lib/kward/prompt_interface/editor/renderer.rb +8 -8
- data/lib/kward/prompt_interface/editor/state.rb +3 -9
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +184 -9
- data/lib/kward/prompt_interface/file_overlay.rb +2 -2
- data/lib/kward/prompt_interface/git_prompt.rb +7 -3
- data/lib/kward/prompt_interface/interactive/controller.rb +4 -4
- data/lib/kward/prompt_interface/interactive/renderer.rb +4 -1
- data/lib/kward/prompt_interface/interactive/state.rb +2 -2
- data/lib/kward/prompt_interface/project_browser.rb +25 -6
- data/lib/kward/prompt_interface/runtime_state.rb +22 -6
- data/lib/kward/prompt_interface/screen.rb +3 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -1
- data/lib/kward/prompt_interface/transcript_buffer.rb +24 -2
- data/lib/kward/prompt_interface.rb +53 -12
- data/lib/kward/prompts/commands.rb +1 -1
- data/lib/kward/prompts.rb +6 -5
- data/lib/kward/rpc/auth_manager.rb +112 -152
- data/lib/kward/rpc/config_manager.rb +46 -6
- data/lib/kward/rpc/plugin_chat_manager.rb +55 -196
- data/lib/kward/rpc/prompt_bridge.rb +8 -4
- data/lib/kward/rpc/redactor.rb +1 -1
- data/lib/kward/rpc/server.rb +60 -10
- data/lib/kward/rpc/session_manager.rb +109 -30
- data/lib/kward/session_store.rb +84 -24
- data/lib/kward/tab_driver.rb +6 -3
- data/lib/kward/tools/git_commit.rb +39 -0
- data/lib/kward/tools/registry.rb +7 -2
- data/lib/kward/tools/tool_call.rb +1 -0
- data/lib/kward/transport/gateway.rb +253 -0
- data/lib/kward/transport/host.rb +343 -0
- data/lib/kward/transport/manager.rb +179 -0
- data/lib/kward/transport/plugin_chat_gateway.rb +186 -0
- data/lib/kward/transport/runtime.rb +41 -0
- data/lib/kward/transport/store.rb +101 -0
- data/lib/kward/transport.rb +204 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace_factory.rb +19 -2
- data/templates/default/fulldoc/html/js/kward.js +2 -0
- data/templates/default/kward_navigation.rb +7 -2
- data/templates/default/layout/html/setup.rb +4 -0
- metadata +26 -1
|
@@ -1,40 +1,33 @@
|
|
|
1
|
-
require "securerandom"
|
|
2
|
-
require "thread"
|
|
3
1
|
require_relative "../cancellation"
|
|
4
|
-
require_relative "../events"
|
|
5
2
|
require_relative "../image_attachments"
|
|
6
|
-
require_relative "../
|
|
3
|
+
require_relative "../plugin_chat_runtime"
|
|
7
4
|
require_relative "../plugin_registry"
|
|
8
|
-
require_relative "../tools/tool_call"
|
|
9
|
-
require_relative "../tab_driver"
|
|
10
5
|
require_relative "attachment_normalizer"
|
|
11
6
|
require_relative "transcript_normalizer"
|
|
12
7
|
|
|
13
8
|
# Namespace for the Kward CLI agent runtime.
|
|
14
9
|
module Kward
|
|
15
10
|
module RPC
|
|
16
|
-
#
|
|
17
|
-
# Unlike SessionManager, this manager never creates workspace sessions,
|
|
18
|
-
# agents, or tool registries: each plugin driver owns its chat and storage.
|
|
11
|
+
# Exposes the frontend-neutral plugin chat runtime over JSON-RPC.
|
|
19
12
|
class PluginChatManager
|
|
20
|
-
EVENT_LIMIT =
|
|
21
|
-
WORKER_STOP = Object.new.freeze
|
|
22
|
-
|
|
23
|
-
Chat = Struct.new(:id, :type, :driver, :queue, :worker, :running_turn_id, keyword_init: true)
|
|
24
|
-
Turn = Struct.new(:id, :chat_id, :input, :display_input, :status, :cancellation, :created_at, :started_at, :finished_at, :events, :next_sequence, :error, :mutex, keyword_init: true)
|
|
13
|
+
EVENT_LIMIT = PluginChatRuntime::EVENT_LIMIT
|
|
25
14
|
|
|
26
15
|
def initialize(server:, client: Client.new, plugin_registry_provider: nil)
|
|
27
16
|
@server = server
|
|
28
|
-
@
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
@runtime = PluginChatRuntime.new(
|
|
18
|
+
client: client,
|
|
19
|
+
plugin_registry_provider: plugin_registry_provider,
|
|
20
|
+
message_normalizer: ->(message) { TranscriptNormalizer.new([message]).normalize.first }
|
|
21
|
+
)
|
|
32
22
|
@subscriptions = {}
|
|
33
23
|
@mutex = Mutex.new
|
|
24
|
+
@runtime.subscribe_events { |event| notify_event(event) }
|
|
34
25
|
end
|
|
35
26
|
|
|
27
|
+
attr_reader :runtime
|
|
28
|
+
|
|
36
29
|
def supported_types
|
|
37
|
-
|
|
30
|
+
@runtime.supported_types(surface: :rpc)
|
|
38
31
|
end
|
|
39
32
|
|
|
40
33
|
def list
|
|
@@ -42,11 +35,11 @@ module Kward
|
|
|
42
35
|
end
|
|
43
36
|
|
|
44
37
|
def open(type_id:)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return
|
|
38
|
+
chat = @runtime.open(type_id: type_id, surface: :rpc, scope_key: "owner")
|
|
39
|
+
payload = chat_payload(chat)
|
|
40
|
+
return payload if chat.driver.respond_to?(:transcript_page)
|
|
48
41
|
|
|
49
|
-
|
|
42
|
+
payload.merge(messages: TranscriptNormalizer.new(chat.driver.messages).normalize)
|
|
50
43
|
end
|
|
51
44
|
|
|
52
45
|
def transcript(chat_id:, limit: nil, before: nil)
|
|
@@ -75,190 +68,51 @@ module Kward
|
|
|
75
68
|
def start_turn(chat_id:, input:, attachments: [])
|
|
76
69
|
chat = fetch_chat(chat_id)
|
|
77
70
|
normalized_attachments = AttachmentNormalizer.new.normalize(attachments)
|
|
78
|
-
turn =
|
|
79
|
-
id: SecureRandom.uuid,
|
|
71
|
+
turn = @runtime.start_turn(
|
|
80
72
|
chat_id: chat.id,
|
|
81
|
-
input: input_with_attachments(input, normalized_attachments),
|
|
82
|
-
display_input: input.to_s
|
|
83
|
-
status: "queued",
|
|
84
|
-
cancellation: Cancellation.new,
|
|
85
|
-
created_at: Time.now.utc.iso8601(3),
|
|
86
|
-
events: [],
|
|
87
|
-
next_sequence: 1,
|
|
88
|
-
mutex: Mutex.new
|
|
73
|
+
input: @runtime.input_with_attachments(input, normalized_attachments),
|
|
74
|
+
display_input: input.to_s
|
|
89
75
|
)
|
|
90
|
-
@mutex.synchronize { @turns[turn.id] = turn }
|
|
91
|
-
chat.queue << turn.id
|
|
92
|
-
ensure_worker(chat)
|
|
93
|
-
emit_event(turn, "turnQueued", { status: "queued" })
|
|
94
76
|
turn_payload(turn)
|
|
95
77
|
end
|
|
96
78
|
|
|
97
79
|
def cancel_turn(turn_id:)
|
|
98
|
-
|
|
99
|
-
queued = turn.mutex.synchronize do
|
|
100
|
-
turn.cancellation.cancel!
|
|
101
|
-
turn.status == "queued"
|
|
102
|
-
end
|
|
103
|
-
emit_event(turn, "turnCancelRequested", {})
|
|
104
|
-
finish_turn(turn, "canceled") if queued
|
|
105
|
-
turn_payload(turn)
|
|
80
|
+
turn_payload(@runtime.cancel_turn(turn_id: turn_id))
|
|
106
81
|
end
|
|
107
82
|
|
|
108
83
|
def turn_status(turn_id:)
|
|
109
|
-
turn_payload(
|
|
84
|
+
turn_payload(@runtime.turn_status(turn_id: turn_id))
|
|
110
85
|
end
|
|
111
86
|
|
|
112
87
|
def turn_events(turn_id:, after_sequence: 0)
|
|
113
|
-
turn =
|
|
114
|
-
|
|
115
|
-
|
|
88
|
+
turn = @runtime.turn_status(turn_id: turn_id)
|
|
89
|
+
{
|
|
90
|
+
turn: turn_payload(turn),
|
|
91
|
+
events: @runtime.turn_events(turn_id: turn_id, after_sequence: after_sequence)
|
|
92
|
+
}
|
|
116
93
|
end
|
|
117
94
|
|
|
118
95
|
def list_turns(chat_id: nil, active: false)
|
|
119
|
-
turns
|
|
120
|
-
turns.select! { |turn| turn.chat_id == chat_id.to_s } if chat_id
|
|
121
|
-
turns.select! { |turn| %w[queued running].include?(turn.status) } if active
|
|
122
|
-
{ turns: turns.map { |turn| turn_payload(turn) } }
|
|
96
|
+
{ turns: @runtime.list_turns(chat_id: chat_id, active: active).map { |turn| turn_payload(turn) } }
|
|
123
97
|
end
|
|
124
98
|
|
|
125
99
|
def shutdown
|
|
126
|
-
|
|
127
|
-
chats.each do |chat|
|
|
128
|
-
chat.queue << WORKER_STOP if chat.worker&.alive?
|
|
129
|
-
chat.worker&.join(0.2)
|
|
130
|
-
end
|
|
100
|
+
@runtime.shutdown
|
|
131
101
|
end
|
|
132
102
|
|
|
133
103
|
private
|
|
134
104
|
|
|
135
|
-
def plugin_registry
|
|
136
|
-
return @plugin_registry_provider.call if @plugin_registry_provider
|
|
137
|
-
|
|
138
|
-
@plugin_registry ||= PluginRegistry.load
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def chat_for(type)
|
|
142
|
-
@mutex.synchronize do
|
|
143
|
-
@chats[type.id] ||= begin
|
|
144
|
-
descriptor = { "kind" => "plugin", "plugin_tab_type" => type.id, "label" => type.title }
|
|
145
|
-
driver = type.handler.call(PluginTabHost.new(client: @client, workspace_root: Dir.pwd), descriptor)
|
|
146
|
-
raise "Plugin chat #{type.id.inspect} did not return a tab driver." unless driver
|
|
147
|
-
|
|
148
|
-
Chat.new(id: type.id, type: type, driver: driver, queue: Queue.new)
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
105
|
def fetch_chat(chat_id)
|
|
154
|
-
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def fetch_turn(turn_id)
|
|
158
|
-
@mutex.synchronize { @turns[turn_id.to_s] } || raise(ArgumentError, "Unknown plugin chat turn: #{turn_id}")
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def ensure_worker(chat)
|
|
162
|
-
return if chat.worker&.alive?
|
|
163
|
-
|
|
164
|
-
chat.worker = Thread.new do
|
|
165
|
-
loop do
|
|
166
|
-
turn_id = chat.queue.pop
|
|
167
|
-
break if turn_id.equal?(WORKER_STOP)
|
|
168
|
-
|
|
169
|
-
turn = fetch_turn(turn_id)
|
|
170
|
-
run_turn(chat, turn) unless turn.cancellation.cancelled?
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
chat.worker.report_on_exception = false
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def run_turn(chat, turn)
|
|
177
|
-
turn.mutex.synchronize do
|
|
178
|
-
return if terminal?(turn)
|
|
179
|
-
|
|
180
|
-
turn.status = "running"
|
|
181
|
-
turn.started_at = Time.now.utc.iso8601(3)
|
|
182
|
-
end
|
|
183
|
-
chat.running_turn_id = turn.id
|
|
184
|
-
emit_event(turn, "turnStarted", { status: "running" })
|
|
185
|
-
chat.driver.submit(turn.input, display_input: turn.display_input, cancellation: turn.cancellation) do |event|
|
|
186
|
-
handle_driver_event(chat, turn, event)
|
|
187
|
-
end
|
|
188
|
-
finish_turn(turn, turn.cancellation.cancelled? ? "canceled" : "completed")
|
|
189
|
-
rescue Cancellation::CancelledError
|
|
190
|
-
finish_turn(turn, "canceled")
|
|
191
|
-
rescue StandardError => e
|
|
192
|
-
turn.mutex.synchronize { turn.error = { message: e.message, code: e.class.name, fatal: false } }
|
|
193
|
-
finish_turn(turn, "failed")
|
|
194
|
-
ensure
|
|
195
|
-
chat.running_turn_id = nil if chat.running_turn_id == turn.id
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def handle_driver_event(chat, turn, event)
|
|
199
|
-
notify_plugin_tab_transcript_event(chat, event) if chat.type.transcript_events
|
|
200
|
-
|
|
201
|
-
type, payload = case event
|
|
202
|
-
when Events::ReasoningDelta then ["reasoningDelta", { delta: event.delta }]
|
|
203
|
-
when Events::ReasoningBoundary then ["reasoningBoundary", {}]
|
|
204
|
-
when Events::AssistantDelta then ["assistantDelta", { delta: event.delta }]
|
|
205
|
-
when Events::AssistantMessage then ["assistantMessage", { message: TranscriptNormalizer.new([event.message]).normalize.first }]
|
|
206
|
-
when Events::Retry then ["modelRetry", retry_event_payload(event)]
|
|
207
|
-
when Events::ToolCall then ["toolCall", tool_call_payload(event.tool_call)]
|
|
208
|
-
when Events::ToolResult then ["toolResult", tool_result_payload(event.tool_call, event.content)]
|
|
209
|
-
when Events::Answer then ["answer", { content: event.content }]
|
|
210
|
-
end
|
|
211
|
-
emit_event(turn, type, payload) if type
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def notify_plugin_tab_transcript_event(chat, event)
|
|
215
|
-
return if plugin_registry.transcript_event_handlers.empty?
|
|
216
|
-
|
|
217
|
-
context = PluginRegistry::Context.new(conversation: chat.driver, workspace_root: Dir.pwd)
|
|
218
|
-
plugin_registry.notify_transcript_event(event, context)
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def retry_event_payload(event)
|
|
222
|
-
{ provider: event.provider, model: event.model, attempt: event.attempt, maxAttempts: event.max_attempts, delaySeconds: event.delay_seconds, error: event.error, requestBytes: event.request_bytes }.compact
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def tool_call_payload(tool_call)
|
|
226
|
-
{ toolCallId: ToolCall.id(tool_call), toolName: ToolCall.name(tool_call), args: ToolCall.parse_arguments(ToolCall.raw_arguments(tool_call)) }.compact
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def tool_result_payload(tool_call, content)
|
|
230
|
-
{ toolCallId: ToolCall.id(tool_call), toolName: ToolCall.name(tool_call), result: { content: content.to_s, isError: content.to_s.start_with?("Unknown", "Invalid", "Research tool failed") } }.compact
|
|
231
|
-
end
|
|
106
|
+
return @runtime.chat(chat_id) if @runtime.chat(chat_id)
|
|
232
107
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
108
|
+
chat_id = chat_id.to_s
|
|
109
|
+
type = supported_types.find { |entry| chat_id == entry.id || chat_id.start_with?("#{entry.id}:") }
|
|
110
|
+
raise ArgumentError, "Unknown plugin chat: #{chat_id}" unless type
|
|
236
111
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
append_event(turn, "turnFinished", { status: status, error: turn.error }.compact)
|
|
240
|
-
end
|
|
241
|
-
notify_event(event) if event
|
|
242
|
-
end
|
|
112
|
+
chat = @runtime.open(type_id: type.id, surface: :rpc, scope_key: "owner")
|
|
113
|
+
raise ArgumentError, "Unknown plugin chat: #{chat_id}" unless chat.id == chat_id
|
|
243
114
|
|
|
244
|
-
|
|
245
|
-
event = turn.mutex.synchronize { append_event(turn, type, payload) }
|
|
246
|
-
notify_event(event)
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def append_event(turn, type, payload)
|
|
250
|
-
event = {
|
|
251
|
-
sequence: turn.next_sequence,
|
|
252
|
-
timestamp: Time.now.utc.iso8601(3),
|
|
253
|
-
chatId: turn.chat_id,
|
|
254
|
-
turnId: turn.id,
|
|
255
|
-
type: type,
|
|
256
|
-
payload: payload
|
|
257
|
-
}
|
|
258
|
-
turn.next_sequence += 1
|
|
259
|
-
turn.events << event
|
|
260
|
-
turn.events.shift while turn.events.length > EVENT_LIMIT
|
|
261
|
-
event
|
|
115
|
+
chat
|
|
262
116
|
end
|
|
263
117
|
|
|
264
118
|
def notify_event(event)
|
|
@@ -266,20 +120,14 @@ module Kward
|
|
|
266
120
|
@server.notify("pluginChat/event", event) if subscribed
|
|
267
121
|
end
|
|
268
122
|
|
|
269
|
-
def input_with_attachments(input, attachments)
|
|
270
|
-
return input.to_s if attachments.empty?
|
|
271
|
-
|
|
272
|
-
[{ type: "text", text: input.to_s }] + attachments.map do |attachment|
|
|
273
|
-
{ type: "image", media_type: attachment[:mimeType], data: attachment[:data], alt: attachment[:alt] }
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def terminal?(turn)
|
|
278
|
-
%w[completed failed canceled].include?(turn.status)
|
|
279
|
-
end
|
|
280
|
-
|
|
281
123
|
def type_payload(type)
|
|
282
|
-
{
|
|
124
|
+
{
|
|
125
|
+
id: type.id,
|
|
126
|
+
name: type.name,
|
|
127
|
+
title: type.title,
|
|
128
|
+
singleton: type.singleton,
|
|
129
|
+
transport: type.transport == true ? true : nil
|
|
130
|
+
}.compact
|
|
283
131
|
end
|
|
284
132
|
|
|
285
133
|
def transcript_page(driver, limit:, before:)
|
|
@@ -289,12 +137,23 @@ module Kward
|
|
|
289
137
|
end
|
|
290
138
|
|
|
291
139
|
def chat_payload(chat)
|
|
292
|
-
type_payload(chat.type).merge(
|
|
140
|
+
type_payload(chat.type).merge(
|
|
141
|
+
id: chat.id,
|
|
142
|
+
subscribed: @mutex.synchronize { @subscriptions[chat.id] == true }
|
|
143
|
+
)
|
|
293
144
|
end
|
|
294
145
|
|
|
295
146
|
def turn_payload(turn)
|
|
296
147
|
turn.mutex.synchronize do
|
|
297
|
-
{
|
|
148
|
+
{
|
|
149
|
+
id: turn.id,
|
|
150
|
+
chatId: turn.chat_id,
|
|
151
|
+
status: turn.status,
|
|
152
|
+
createdAt: turn.created_at,
|
|
153
|
+
startedAt: turn.started_at,
|
|
154
|
+
finishedAt: turn.finished_at,
|
|
155
|
+
error: turn.error
|
|
156
|
+
}.compact
|
|
298
157
|
end
|
|
299
158
|
end
|
|
300
159
|
end
|
|
@@ -8,8 +8,12 @@ module Kward
|
|
|
8
8
|
module RPC
|
|
9
9
|
# RPC prompt bridge for structured user questions.
|
|
10
10
|
class PromptBridge
|
|
11
|
-
def initialize(server:,
|
|
12
|
-
@
|
|
11
|
+
def initialize(server: nil, session_id:, notify: nil)
|
|
12
|
+
@notify = notify || lambda do |method, payload|
|
|
13
|
+
raise ArgumentError, "PromptBridge requires a server or notify callback" unless server
|
|
14
|
+
|
|
15
|
+
server.notify(method, payload)
|
|
16
|
+
end
|
|
13
17
|
@session_id = session_id
|
|
14
18
|
@mutex = Mutex.new
|
|
15
19
|
@condition = ConditionVariable.new
|
|
@@ -22,7 +26,7 @@ module Kward
|
|
|
22
26
|
@mutex.synchronize { @pending_requests[request_id] = true }
|
|
23
27
|
cancellation&.on_cancel { cancel_request(request_id) }
|
|
24
28
|
unless cancellation&.cancelled?
|
|
25
|
-
@
|
|
29
|
+
@notify.call("tool/approvalRequested", {
|
|
26
30
|
sessionId: @session_id,
|
|
27
31
|
approvalRequestId: request_id,
|
|
28
32
|
toolCallId: tool_call_id,
|
|
@@ -49,7 +53,7 @@ module Kward
|
|
|
49
53
|
@mutex.synchronize { @pending_requests[request_id] = true }
|
|
50
54
|
cancellation&.on_cancel { cancel_request(request_id) }
|
|
51
55
|
unless cancellation&.cancelled?
|
|
52
|
-
@
|
|
56
|
+
@notify.call("ui/question", {
|
|
53
57
|
sessionId: @session_id,
|
|
54
58
|
questionRequestId: request_id,
|
|
55
59
|
questions: questions
|
data/lib/kward/rpc/redactor.rb
CHANGED
data/lib/kward/rpc/server.rb
CHANGED
|
@@ -3,6 +3,7 @@ require_relative "../model/client"
|
|
|
3
3
|
require_relative "../config_files"
|
|
4
4
|
require_relative "../memory/manager"
|
|
5
5
|
require_relative "../plugin_registry"
|
|
6
|
+
require_relative "../transport"
|
|
6
7
|
require_relative "../prompts/commands"
|
|
7
8
|
require_relative "../tools/registry"
|
|
8
9
|
require_relative "../workspace_factory"
|
|
@@ -15,6 +16,7 @@ require_relative "redactor"
|
|
|
15
16
|
require_relative "session_manager"
|
|
16
17
|
require_relative "plugin_chat_manager"
|
|
17
18
|
require_relative "transport"
|
|
19
|
+
require_relative "../transport/plugin_chat_gateway"
|
|
18
20
|
|
|
19
21
|
# Namespace for the Kward CLI agent runtime.
|
|
20
22
|
module Kward
|
|
@@ -59,7 +61,7 @@ module Kward
|
|
|
59
61
|
].freeze
|
|
60
62
|
TURN_METHODS = ["turns/start", "turns/cancel", "turns/status", "turns/events", "turns/list", "turns/listActive"].freeze
|
|
61
63
|
PLUGIN_CHAT_METHODS = ["pluginChats/list", "pluginChats/open", "pluginChats/transcript", "pluginChats/subscribe", "pluginChats/unsubscribe", "pluginChats/turns/start", "pluginChats/turns/cancel", "pluginChats/turns/status", "pluginChats/turns/events", "pluginChats/turns/list", "pluginChats/turns/listActive"].freeze
|
|
62
|
-
MODEL_METHODS = ["models/list", "models/current", "models/set", "reasoning/set"].freeze
|
|
64
|
+
MODEL_METHODS = ["models/list", "models/current", "models/set", "reasoning/set", "models/refresh"].freeze
|
|
63
65
|
RUNTIME_METHODS = ["runtime/state", "runtime/stats"].freeze
|
|
64
66
|
RUNTIME_SETTING_METHODS = ["runtime/updateSetting", "runtime/reload"].freeze
|
|
65
67
|
AUTH_METHODS = [
|
|
@@ -80,6 +82,7 @@ module Kward
|
|
|
80
82
|
LIFECYCLE_HOOK_METHODS = ["hooks/logs"].freeze
|
|
81
83
|
UI_METHODS = ["ui/answerQuestion"].freeze
|
|
82
84
|
TOOL_APPROVAL_METHODS = ["tool/answerApproval"].freeze
|
|
85
|
+
TRANSPORT_METHODS = ["transports/list", "transports/status"].freeze
|
|
83
86
|
SESSION_EVENT_NOTIFICATION = "session/event"
|
|
84
87
|
SESSION_UPDATED_NOTIFICATION = "session/updated"
|
|
85
88
|
TURN_EVENT_NOTIFICATION = "turn/event"
|
|
@@ -129,10 +132,23 @@ module Kward
|
|
|
129
132
|
client: client,
|
|
130
133
|
plugin_registry_provider: -> { @session_manager.plugin_registry }
|
|
131
134
|
)
|
|
135
|
+
@transport_manager = Kward::Transport::Manager.new(
|
|
136
|
+
registry: @session_manager.plugin_registry,
|
|
137
|
+
gateway: lambda { |transport_id|
|
|
138
|
+
Kward::Transport::Gateway.new(session_manager: @session_manager, transport_id: transport_id)
|
|
139
|
+
},
|
|
140
|
+
plugin_chat_gateway: lambda { |transport_id|
|
|
141
|
+
Kward::Transport::PluginChatGateway.new(runtime: @plugin_chat_manager.runtime, transport_id: transport_id)
|
|
142
|
+
},
|
|
143
|
+
config_provider: ->(transport_id) { ConfigFiles.transport_config(transport_id) }
|
|
144
|
+
)
|
|
132
145
|
@auth_manager = AuthManager.new(server: self, config_manager: @config_manager)
|
|
133
146
|
@shutdown = false
|
|
147
|
+
@shutdown_complete = false
|
|
134
148
|
end
|
|
135
149
|
|
|
150
|
+
attr_reader :session_manager, :plugin_chat_manager
|
|
151
|
+
|
|
136
152
|
# Reads framed JSON-RPC messages until shutdown or EOF.
|
|
137
153
|
#
|
|
138
154
|
# @return [void]
|
|
@@ -150,8 +166,17 @@ module Kward
|
|
|
150
166
|
end
|
|
151
167
|
end
|
|
152
168
|
ensure
|
|
169
|
+
shutdown
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def shutdown
|
|
173
|
+
return if @shutdown_complete
|
|
174
|
+
|
|
175
|
+
@shutdown = true
|
|
176
|
+
@transport_manager.shutdown
|
|
153
177
|
@plugin_chat_manager.shutdown
|
|
154
178
|
@session_manager.shutdown_sessions
|
|
179
|
+
@shutdown_complete = true
|
|
155
180
|
end
|
|
156
181
|
|
|
157
182
|
# Sends a redacted JSON-RPC notification to the client.
|
|
@@ -229,6 +254,10 @@ module Kward
|
|
|
229
254
|
@session_manager.tool_schemas(session_id: params["sessionId"])
|
|
230
255
|
when MCP_METHODS[0]
|
|
231
256
|
MCPStatus.new(config_manager: @config_manager).to_h
|
|
257
|
+
when TRANSPORT_METHODS[0]
|
|
258
|
+
{ transports: @transport_manager.list }
|
|
259
|
+
when TRANSPORT_METHODS[1]
|
|
260
|
+
params["name"] ? @transport_manager.status(params["name"]) : { transports: @transport_manager.status }
|
|
232
261
|
when PROMPT_METHODS[0]
|
|
233
262
|
prompts_list
|
|
234
263
|
when PROMPT_METHODS[1]
|
|
@@ -241,6 +270,8 @@ module Kward
|
|
|
241
270
|
models_set(params)
|
|
242
271
|
when MODEL_METHODS[3]
|
|
243
272
|
reasoning_set(params)
|
|
273
|
+
when MODEL_METHODS[4]
|
|
274
|
+
models_refresh(params)
|
|
244
275
|
when RUNTIME_METHODS[0]
|
|
245
276
|
@session_manager.runtime_state(session_id: params.fetch("sessionId"))
|
|
246
277
|
when RUNTIME_METHODS[1]
|
|
@@ -429,7 +460,14 @@ module Kward
|
|
|
429
460
|
compact: { supported: true, method: SESSION_METHODS[5], notification: SESSION_EVENT_NOTIFICATION, events: ["compactionStart", "compactionEnd"] },
|
|
430
461
|
import: { supported: false },
|
|
431
462
|
tree: { supported: true, method: SESSION_METHODS[8], labels: true, labelTimestamps: true, navigate: true, summarize: true, shape: "kward-tree-items-v1" },
|
|
432
|
-
updates: { supported: false, notification: SESSION_UPDATED_NOTIFICATION }
|
|
463
|
+
updates: { supported: false, notification: SESSION_UPDATED_NOTIFICATION },
|
|
464
|
+
worktrees: { supported: false, reason: "interactiveTuiOnly" }
|
|
465
|
+
},
|
|
466
|
+
transports: {
|
|
467
|
+
supported: true,
|
|
468
|
+
methods: TRANSPORT_METHODS,
|
|
469
|
+
startMode: "cliOnly",
|
|
470
|
+
entries: @transport_manager.list
|
|
433
471
|
},
|
|
434
472
|
pluginChats: {
|
|
435
473
|
supported: @plugin_chat_manager.supported_types.any?,
|
|
@@ -437,7 +475,7 @@ module Kward
|
|
|
437
475
|
notification: "pluginChat/event",
|
|
438
476
|
subscriptions: { supported: true, methods: PLUGIN_CHAT_METHODS.values_at(3, 4), requiredForLiveEvents: true },
|
|
439
477
|
attachments: { supported: true, method: PLUGIN_CHAT_METHODS[5], encoding: "base64", mimeTypes: SessionManager::RPC_IMAGE_MIME_TYPES, maxBytes: SessionManager::RPC_ATTACHMENT_MAX_BYTES },
|
|
440
|
-
types: @plugin_chat_manager.supported_types.map { |type| { id: type.id, name: type.name, title: type.title, singleton: type.singleton }.compact }
|
|
478
|
+
types: @plugin_chat_manager.supported_types.map { |type| { id: type.id, name: type.name, title: type.title, singleton: type.singleton, transport: type.transport == true ? true : nil }.compact }
|
|
441
479
|
},
|
|
442
480
|
turns: {
|
|
443
481
|
mode: "async",
|
|
@@ -493,7 +531,7 @@ module Kward
|
|
|
493
531
|
methods: MODEL_METHODS,
|
|
494
532
|
fields: ["provider", "id", "name", "reasoning", "reasoningEffort", "contextWindow"],
|
|
495
533
|
scopedModels: false,
|
|
496
|
-
|
|
534
|
+
refresh: { supported: true, method: MODEL_METHODS[4], providerParameter: true }
|
|
497
535
|
},
|
|
498
536
|
runtime: {
|
|
499
537
|
supported: true,
|
|
@@ -522,11 +560,19 @@ module Kward
|
|
|
522
560
|
},
|
|
523
561
|
auth: {
|
|
524
562
|
supported: true,
|
|
525
|
-
providerFormat: "kward-auth-
|
|
563
|
+
providerFormat: "kward-auth-v2",
|
|
526
564
|
methods: AUTH_METHODS,
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
565
|
+
providerCatalog: true,
|
|
566
|
+
authenticationMethods: ["api_key", "oauth"],
|
|
567
|
+
oauthProviders: ["openai", "anthropic"],
|
|
568
|
+
unsupportedOAuthProviders: {
|
|
569
|
+
copilot: "OAuth login is available only in the interactive CLI.",
|
|
570
|
+
openrouter: "Official PKCE is documented, but Kward has not implemented it.",
|
|
571
|
+
xai: "No official stable third-party OAuth flow is available."
|
|
572
|
+
},
|
|
573
|
+
apiKeyProviders: ProviderCatalog.api_key_providers.map(&:id),
|
|
574
|
+
privateCredentialStorage: true,
|
|
575
|
+
sanitizedStatus: true,
|
|
530
576
|
logout: true
|
|
531
577
|
},
|
|
532
578
|
memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: MEMORY_METHODS },
|
|
@@ -775,17 +821,21 @@ module Kward
|
|
|
775
821
|
end
|
|
776
822
|
|
|
777
823
|
def auth_login_with_api_key(params)
|
|
778
|
-
result = @auth_manager.login_with_api_key(provider_id: params.fetch("providerId"), api_key: params.fetch("apiKey"))
|
|
824
|
+
result = @auth_manager.login_with_api_key(provider_id: params.fetch("providerId"), api_key: params.fetch("apiKey"), configuration: params["configuration"])
|
|
779
825
|
@session_manager.refresh_client_config
|
|
780
826
|
result
|
|
781
827
|
end
|
|
782
828
|
|
|
783
829
|
def auth_logout_provider(params)
|
|
784
|
-
result = @auth_manager.logout_provider(provider_id: params.fetch("providerId"))
|
|
830
|
+
result = @auth_manager.logout_provider(provider_id: params.fetch("providerId"), auth_method: params["authMethod"])
|
|
785
831
|
@session_manager.refresh_client_config
|
|
786
832
|
result
|
|
787
833
|
end
|
|
788
834
|
|
|
835
|
+
def models_refresh(params)
|
|
836
|
+
{ models: @session_manager.refresh_models(provider: params["provider"]) }
|
|
837
|
+
end
|
|
838
|
+
|
|
789
839
|
def models_current
|
|
790
840
|
@session_manager.current_model
|
|
791
841
|
end
|