kward 0.78.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 +13 -2
- data/.github/workflows/pages.yml +1 -1
- data/CHANGELOG.md +82 -1
- data/Gemfile.lock +2 -2
- data/README.md +33 -38
- data/doc/agent-tools.md +10 -11
- 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 +136 -27
- data/doc/context-budgeting.md +6 -6
- data/doc/context-tools.md +2 -2
- data/doc/editor.md +7 -7
- data/doc/extensibility.md +9 -29
- data/doc/files.md +6 -6
- data/doc/getting-started.md +3 -3
- data/doc/git.md +8 -2
- data/doc/lifecycle-hooks.md +5 -4
- data/doc/local-models.md +130 -0
- data/doc/mcp.md +5 -5
- data/doc/permissions.md +15 -5
- data/doc/plugins.md +39 -4
- data/doc/prompt-templates.md +105 -0
- data/doc/providers.md +140 -0
- data/doc/releasing.md +11 -3
- data/doc/rpc.md +65 -31
- data/doc/sandboxing.md +142 -0
- data/doc/security.md +31 -10
- data/doc/session-management.md +2 -4
- data/doc/skills.md +10 -0
- 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 +4 -4
- 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/kward.gemspec +1 -1
- 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/doctor.rb +21 -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 +162 -36
- data/lib/kward/cli/slash_commands.rb +109 -1
- 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 +22 -1
- data/lib/kward/config_files.rb +106 -4
- data/lib/kward/conversation.rb +27 -8
- data/lib/kward/git_worktree_manager.rb +291 -0
- data/lib/kward/local_command_runner.rb +2 -2
- data/lib/kward/model/azure_openai_config.rb +54 -0
- data/lib/kward/model/catalog.rb +113 -0
- data/lib/kward/model/client.rb +364 -23
- data/lib/kward/model/model_info.rb +60 -1
- data/lib/kward/model/payloads.rb +64 -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 +141 -39
- data/lib/kward/pan/index.html.erb +50 -0
- data/lib/kward/pan/server.rb +49 -2
- data/lib/kward/permissions/policy.rb +18 -4
- data/lib/kward/plugin_chat_runtime.rb +374 -0
- data/lib/kward/plugin_registry.rb +81 -8
- 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 +43 -10
- 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 +71 -12
- data/lib/kward/prompts/commands.rb +3 -1
- data/lib/kward/prompts.rb +22 -10
- 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 +56 -194
- data/lib/kward/rpc/prompt_bridge.rb +8 -4
- data/lib/kward/rpc/redactor.rb +1 -1
- data/lib/kward/rpc/server.rb +102 -14
- data/lib/kward/rpc/session_manager.rb +151 -38
- data/lib/kward/sandbox/capabilities.rb +39 -0
- data/lib/kward/sandbox/command_runner.rb +28 -0
- data/lib/kward/sandbox/environment.rb +24 -0
- data/lib/kward/sandbox/linux_bubblewrap_runner.rb +71 -0
- data/lib/kward/sandbox/macos_seatbelt_runner.rb +96 -0
- data/lib/kward/sandbox/passthrough_runner.rb +13 -0
- data/lib/kward/sandbox/policy.rb +74 -0
- data/lib/kward/sandbox/runner_factory.rb +55 -0
- data/lib/kward/sandbox/unavailable_runner.rb +21 -0
- data/lib/kward/sandbox.rb +9 -0
- data/lib/kward/session_store.rb +110 -24
- data/lib/kward/skills/capture.rb +144 -0
- data/lib/kward/tab_driver.rb +6 -3
- data/lib/kward/tools/git_commit.rb +39 -0
- data/lib/kward/tools/registry.rb +26 -6
- 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.rb +18 -3
- data/lib/kward/workspace_factory.rb +34 -0
- data/templates/default/fulldoc/html/js/kward.js +3 -0
- data/templates/default/kward_navigation.rb +8 -1
- data/templates/default/layout/html/setup.rb +5 -0
- metadata +41 -2
|
@@ -1,39 +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
|
|
13
|
+
EVENT_LIMIT = PluginChatRuntime::EVENT_LIMIT
|
|
22
14
|
|
|
23
|
-
|
|
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)
|
|
25
|
-
|
|
26
|
-
def initialize(server:, client: Client.new)
|
|
15
|
+
def initialize(server:, client: Client.new, plugin_registry_provider: nil)
|
|
27
16
|
@server = server
|
|
28
|
-
@
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
@runtime = PluginChatRuntime.new(
|
|
18
|
+
client: client,
|
|
19
|
+
plugin_registry_provider: plugin_registry_provider,
|
|
20
|
+
message_normalizer: ->(message) { TranscriptNormalizer.new([message]).normalize.first }
|
|
21
|
+
)
|
|
31
22
|
@subscriptions = {}
|
|
32
23
|
@mutex = Mutex.new
|
|
24
|
+
@runtime.subscribe_events { |event| notify_event(event) }
|
|
33
25
|
end
|
|
34
26
|
|
|
27
|
+
attr_reader :runtime
|
|
28
|
+
|
|
35
29
|
def supported_types
|
|
36
|
-
|
|
30
|
+
@runtime.supported_types(surface: :rpc)
|
|
37
31
|
end
|
|
38
32
|
|
|
39
33
|
def list
|
|
@@ -41,11 +35,11 @@ module Kward
|
|
|
41
35
|
end
|
|
42
36
|
|
|
43
37
|
def open(type_id:)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
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)
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
payload.merge(messages: TranscriptNormalizer.new(chat.driver.messages).normalize)
|
|
49
43
|
end
|
|
50
44
|
|
|
51
45
|
def transcript(chat_id:, limit: nil, before: nil)
|
|
@@ -74,188 +68,51 @@ module Kward
|
|
|
74
68
|
def start_turn(chat_id:, input:, attachments: [])
|
|
75
69
|
chat = fetch_chat(chat_id)
|
|
76
70
|
normalized_attachments = AttachmentNormalizer.new.normalize(attachments)
|
|
77
|
-
turn =
|
|
78
|
-
id: SecureRandom.uuid,
|
|
71
|
+
turn = @runtime.start_turn(
|
|
79
72
|
chat_id: chat.id,
|
|
80
|
-
input: input_with_attachments(input, normalized_attachments),
|
|
81
|
-
display_input: input.to_s
|
|
82
|
-
status: "queued",
|
|
83
|
-
cancellation: Cancellation.new,
|
|
84
|
-
created_at: Time.now.utc.iso8601(3),
|
|
85
|
-
events: [],
|
|
86
|
-
next_sequence: 1,
|
|
87
|
-
mutex: Mutex.new
|
|
73
|
+
input: @runtime.input_with_attachments(input, normalized_attachments),
|
|
74
|
+
display_input: input.to_s
|
|
88
75
|
)
|
|
89
|
-
@mutex.synchronize { @turns[turn.id] = turn }
|
|
90
|
-
chat.queue << turn.id
|
|
91
|
-
ensure_worker(chat)
|
|
92
|
-
emit_event(turn, "turnQueued", { status: "queued" })
|
|
93
76
|
turn_payload(turn)
|
|
94
77
|
end
|
|
95
78
|
|
|
96
79
|
def cancel_turn(turn_id:)
|
|
97
|
-
|
|
98
|
-
queued = turn.mutex.synchronize do
|
|
99
|
-
turn.cancellation.cancel!
|
|
100
|
-
turn.status == "queued"
|
|
101
|
-
end
|
|
102
|
-
emit_event(turn, "turnCancelRequested", {})
|
|
103
|
-
finish_turn(turn, "canceled") if queued
|
|
104
|
-
turn_payload(turn)
|
|
80
|
+
turn_payload(@runtime.cancel_turn(turn_id: turn_id))
|
|
105
81
|
end
|
|
106
82
|
|
|
107
83
|
def turn_status(turn_id:)
|
|
108
|
-
turn_payload(
|
|
84
|
+
turn_payload(@runtime.turn_status(turn_id: turn_id))
|
|
109
85
|
end
|
|
110
86
|
|
|
111
87
|
def turn_events(turn_id:, after_sequence: 0)
|
|
112
|
-
turn =
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
}
|
|
115
93
|
end
|
|
116
94
|
|
|
117
95
|
def list_turns(chat_id: nil, active: false)
|
|
118
|
-
turns
|
|
119
|
-
turns.select! { |turn| turn.chat_id == chat_id.to_s } if chat_id
|
|
120
|
-
turns.select! { |turn| %w[queued running].include?(turn.status) } if active
|
|
121
|
-
{ turns: turns.map { |turn| turn_payload(turn) } }
|
|
96
|
+
{ turns: @runtime.list_turns(chat_id: chat_id, active: active).map { |turn| turn_payload(turn) } }
|
|
122
97
|
end
|
|
123
98
|
|
|
124
99
|
def shutdown
|
|
125
|
-
|
|
126
|
-
chats.each do |chat|
|
|
127
|
-
chat.queue << WORKER_STOP if chat.worker&.alive?
|
|
128
|
-
chat.worker&.join(0.2)
|
|
129
|
-
end
|
|
100
|
+
@runtime.shutdown
|
|
130
101
|
end
|
|
131
102
|
|
|
132
103
|
private
|
|
133
104
|
|
|
134
|
-
def plugin_registry
|
|
135
|
-
@plugin_registry ||= PluginRegistry.load
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def chat_for(type)
|
|
139
|
-
@mutex.synchronize do
|
|
140
|
-
@chats[type.id] ||= begin
|
|
141
|
-
descriptor = { "kind" => "plugin", "plugin_tab_type" => type.id, "label" => type.title }
|
|
142
|
-
driver = type.handler.call(PluginTabHost.new(client: @client, workspace_root: Dir.pwd), descriptor)
|
|
143
|
-
raise "Plugin chat #{type.id.inspect} did not return a tab driver." unless driver
|
|
144
|
-
|
|
145
|
-
Chat.new(id: type.id, type: type, driver: driver, queue: Queue.new)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
105
|
def fetch_chat(chat_id)
|
|
151
|
-
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def fetch_turn(turn_id)
|
|
155
|
-
@mutex.synchronize { @turns[turn_id.to_s] } || raise(ArgumentError, "Unknown plugin chat turn: #{turn_id}")
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def ensure_worker(chat)
|
|
159
|
-
return if chat.worker&.alive?
|
|
160
|
-
|
|
161
|
-
chat.worker = Thread.new do
|
|
162
|
-
loop do
|
|
163
|
-
turn_id = chat.queue.pop
|
|
164
|
-
break if turn_id.equal?(WORKER_STOP)
|
|
165
|
-
|
|
166
|
-
turn = fetch_turn(turn_id)
|
|
167
|
-
run_turn(chat, turn) unless turn.cancellation.cancelled?
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
chat.worker.report_on_exception = false
|
|
171
|
-
end
|
|
106
|
+
return @runtime.chat(chat_id) if @runtime.chat(chat_id)
|
|
172
107
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
176
111
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
end
|
|
180
|
-
chat.running_turn_id = turn.id
|
|
181
|
-
emit_event(turn, "turnStarted", { status: "running" })
|
|
182
|
-
chat.driver.submit(turn.input, display_input: turn.display_input, cancellation: turn.cancellation) do |event|
|
|
183
|
-
handle_driver_event(chat, turn, event)
|
|
184
|
-
end
|
|
185
|
-
finish_turn(turn, turn.cancellation.cancelled? ? "canceled" : "completed")
|
|
186
|
-
rescue Cancellation::CancelledError
|
|
187
|
-
finish_turn(turn, "canceled")
|
|
188
|
-
rescue StandardError => e
|
|
189
|
-
turn.mutex.synchronize { turn.error = { message: e.message, code: e.class.name, fatal: false } }
|
|
190
|
-
finish_turn(turn, "failed")
|
|
191
|
-
ensure
|
|
192
|
-
chat.running_turn_id = nil if chat.running_turn_id == turn.id
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def handle_driver_event(chat, turn, event)
|
|
196
|
-
notify_plugin_tab_transcript_event(chat, event) if chat.type.transcript_events
|
|
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
|
|
197
114
|
|
|
198
|
-
|
|
199
|
-
when Events::ReasoningDelta then ["reasoningDelta", { delta: event.delta }]
|
|
200
|
-
when Events::ReasoningBoundary then ["reasoningBoundary", {}]
|
|
201
|
-
when Events::AssistantDelta then ["assistantDelta", { delta: event.delta }]
|
|
202
|
-
when Events::AssistantMessage then ["assistantMessage", { message: TranscriptNormalizer.new([event.message]).normalize.first }]
|
|
203
|
-
when Events::Retry then ["modelRetry", retry_event_payload(event)]
|
|
204
|
-
when Events::ToolCall then ["toolCall", tool_call_payload(event.tool_call)]
|
|
205
|
-
when Events::ToolResult then ["toolResult", tool_result_payload(event.tool_call, event.content)]
|
|
206
|
-
when Events::Answer then ["answer", { content: event.content }]
|
|
207
|
-
end
|
|
208
|
-
emit_event(turn, type, payload) if type
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def notify_plugin_tab_transcript_event(chat, event)
|
|
212
|
-
return if plugin_registry.transcript_event_handlers.empty?
|
|
213
|
-
|
|
214
|
-
context = PluginRegistry::Context.new(conversation: chat.driver, workspace_root: Dir.pwd)
|
|
215
|
-
plugin_registry.notify_transcript_event(event, context)
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def retry_event_payload(event)
|
|
219
|
-
{ 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
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def tool_call_payload(tool_call)
|
|
223
|
-
{ toolCallId: ToolCall.id(tool_call), toolName: ToolCall.name(tool_call), args: ToolCall.parse_arguments(ToolCall.raw_arguments(tool_call)) }.compact
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def tool_result_payload(tool_call, content)
|
|
227
|
-
{ 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
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def finish_turn(turn, status)
|
|
231
|
-
event = turn.mutex.synchronize do
|
|
232
|
-
next nil if terminal?(turn)
|
|
233
|
-
|
|
234
|
-
turn.status = status
|
|
235
|
-
turn.finished_at = Time.now.utc.iso8601(3)
|
|
236
|
-
append_event(turn, "turnFinished", { status: status, error: turn.error }.compact)
|
|
237
|
-
end
|
|
238
|
-
notify_event(event) if event
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def emit_event(turn, type, payload)
|
|
242
|
-
event = turn.mutex.synchronize { append_event(turn, type, payload) }
|
|
243
|
-
notify_event(event)
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def append_event(turn, type, payload)
|
|
247
|
-
event = {
|
|
248
|
-
sequence: turn.next_sequence,
|
|
249
|
-
timestamp: Time.now.utc.iso8601(3),
|
|
250
|
-
chatId: turn.chat_id,
|
|
251
|
-
turnId: turn.id,
|
|
252
|
-
type: type,
|
|
253
|
-
payload: payload
|
|
254
|
-
}
|
|
255
|
-
turn.next_sequence += 1
|
|
256
|
-
turn.events << event
|
|
257
|
-
turn.events.shift while turn.events.length > EVENT_LIMIT
|
|
258
|
-
event
|
|
115
|
+
chat
|
|
259
116
|
end
|
|
260
117
|
|
|
261
118
|
def notify_event(event)
|
|
@@ -263,20 +120,14 @@ module Kward
|
|
|
263
120
|
@server.notify("pluginChat/event", event) if subscribed
|
|
264
121
|
end
|
|
265
122
|
|
|
266
|
-
def input_with_attachments(input, attachments)
|
|
267
|
-
return input.to_s if attachments.empty?
|
|
268
|
-
|
|
269
|
-
[{ type: "text", text: input.to_s }] + attachments.map do |attachment|
|
|
270
|
-
{ type: "image", media_type: attachment[:mimeType], data: attachment[:data], alt: attachment[:alt] }
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
def terminal?(turn)
|
|
275
|
-
%w[completed failed canceled].include?(turn.status)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
123
|
def type_payload(type)
|
|
279
|
-
{
|
|
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
|
|
280
131
|
end
|
|
281
132
|
|
|
282
133
|
def transcript_page(driver, limit:, before:)
|
|
@@ -286,12 +137,23 @@ module Kward
|
|
|
286
137
|
end
|
|
287
138
|
|
|
288
139
|
def chat_payload(chat)
|
|
289
|
-
type_payload(chat.type).merge(
|
|
140
|
+
type_payload(chat.type).merge(
|
|
141
|
+
id: chat.id,
|
|
142
|
+
subscribed: @mutex.synchronize { @subscriptions[chat.id] == true }
|
|
143
|
+
)
|
|
290
144
|
end
|
|
291
145
|
|
|
292
146
|
def turn_payload(turn)
|
|
293
147
|
turn.mutex.synchronize do
|
|
294
|
-
{
|
|
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
|
|
295
157
|
end
|
|
296
158
|
end
|
|
297
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,9 +3,10 @@ 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
|
-
require_relative "../
|
|
9
|
+
require_relative "../workspace_factory"
|
|
9
10
|
require_relative "../telemetry/logger"
|
|
10
11
|
require_relative "../telemetry/stats"
|
|
11
12
|
require_relative "auth_manager"
|
|
@@ -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 = [
|
|
@@ -73,12 +75,14 @@ module Kward
|
|
|
73
75
|
"memory/why", "memory/summarize"
|
|
74
76
|
].freeze
|
|
75
77
|
COMMAND_METHODS = ["commands/list", "commands/run"].freeze
|
|
78
|
+
SKILL_CAPTURE_METHODS = ["skills/captureSessions", "skills/captureDraft", "skills/saveCapturedDraft"].freeze
|
|
76
79
|
STARTUP_RESOURCE_METHODS = ["resources/startup"].freeze
|
|
77
80
|
CONFIG_METHODS = ["config/read", "config/update"].freeze
|
|
78
81
|
LOGGING_METHODS = ["logging/stats", "logging/tokenCsv"].freeze
|
|
79
82
|
LIFECYCLE_HOOK_METHODS = ["hooks/logs"].freeze
|
|
80
83
|
UI_METHODS = ["ui/answerQuestion"].freeze
|
|
81
84
|
TOOL_APPROVAL_METHODS = ["tool/answerApproval"].freeze
|
|
85
|
+
TRANSPORT_METHODS = ["transports/list", "transports/status"].freeze
|
|
82
86
|
SESSION_EVENT_NOTIFICATION = "session/event"
|
|
83
87
|
SESSION_UPDATED_NOTIFICATION = "session/updated"
|
|
84
88
|
TURN_EVENT_NOTIFICATION = "turn/event"
|
|
@@ -101,6 +105,7 @@ module Kward
|
|
|
101
105
|
auth: AUTH_METHODS,
|
|
102
106
|
memory: MEMORY_METHODS,
|
|
103
107
|
commands: COMMAND_METHODS,
|
|
108
|
+
skill_capture: SKILL_CAPTURE_METHODS,
|
|
104
109
|
startup_resources: STARTUP_RESOURCE_METHODS,
|
|
105
110
|
config: CONFIG_METHODS,
|
|
106
111
|
logging: LOGGING_METHODS,
|
|
@@ -122,11 +127,28 @@ module Kward
|
|
|
122
127
|
@client = client
|
|
123
128
|
@config_manager = ConfigManager.new
|
|
124
129
|
@session_manager = SessionManager.new(server: self, client: client, config_manager: @config_manager)
|
|
125
|
-
@plugin_chat_manager = PluginChatManager.new(
|
|
130
|
+
@plugin_chat_manager = PluginChatManager.new(
|
|
131
|
+
server: self,
|
|
132
|
+
client: client,
|
|
133
|
+
plugin_registry_provider: -> { @session_manager.plugin_registry }
|
|
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
|
+
)
|
|
126
145
|
@auth_manager = AuthManager.new(server: self, config_manager: @config_manager)
|
|
127
146
|
@shutdown = false
|
|
147
|
+
@shutdown_complete = false
|
|
128
148
|
end
|
|
129
149
|
|
|
150
|
+
attr_reader :session_manager, :plugin_chat_manager
|
|
151
|
+
|
|
130
152
|
# Reads framed JSON-RPC messages until shutdown or EOF.
|
|
131
153
|
#
|
|
132
154
|
# @return [void]
|
|
@@ -144,8 +166,17 @@ module Kward
|
|
|
144
166
|
end
|
|
145
167
|
end
|
|
146
168
|
ensure
|
|
169
|
+
shutdown
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def shutdown
|
|
173
|
+
return if @shutdown_complete
|
|
174
|
+
|
|
175
|
+
@shutdown = true
|
|
176
|
+
@transport_manager.shutdown
|
|
147
177
|
@plugin_chat_manager.shutdown
|
|
148
178
|
@session_manager.shutdown_sessions
|
|
179
|
+
@shutdown_complete = true
|
|
149
180
|
end
|
|
150
181
|
|
|
151
182
|
# Sends a redacted JSON-RPC notification to the client.
|
|
@@ -223,6 +254,10 @@ module Kward
|
|
|
223
254
|
@session_manager.tool_schemas(session_id: params["sessionId"])
|
|
224
255
|
when MCP_METHODS[0]
|
|
225
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 }
|
|
226
261
|
when PROMPT_METHODS[0]
|
|
227
262
|
prompts_list
|
|
228
263
|
when PROMPT_METHODS[1]
|
|
@@ -235,6 +270,8 @@ module Kward
|
|
|
235
270
|
models_set(params)
|
|
236
271
|
when MODEL_METHODS[3]
|
|
237
272
|
reasoning_set(params)
|
|
273
|
+
when MODEL_METHODS[4]
|
|
274
|
+
models_refresh(params)
|
|
238
275
|
when RUNTIME_METHODS[0]
|
|
239
276
|
@session_manager.runtime_state(session_id: params.fetch("sessionId"))
|
|
240
277
|
when RUNTIME_METHODS[1]
|
|
@@ -247,6 +284,12 @@ module Kward
|
|
|
247
284
|
commands_list(params)
|
|
248
285
|
when COMMAND_METHODS[1]
|
|
249
286
|
commands_run(params)
|
|
287
|
+
when SKILL_CAPTURE_METHODS[0]
|
|
288
|
+
{ sessions: @session_manager.skill_capture_sessions }
|
|
289
|
+
when SKILL_CAPTURE_METHODS[1]
|
|
290
|
+
@session_manager.capture_skill(session_path: params.fetch("sessionPath"))
|
|
291
|
+
when SKILL_CAPTURE_METHODS[2]
|
|
292
|
+
@session_manager.save_captured_skill(content: params.fetch("content"), overwrite: params["overwrite"] == true)
|
|
250
293
|
when STARTUP_RESOURCE_METHODS[0]
|
|
251
294
|
startup_resources(params)
|
|
252
295
|
when CONFIG_METHODS[0]
|
|
@@ -417,7 +460,14 @@ module Kward
|
|
|
417
460
|
compact: { supported: true, method: SESSION_METHODS[5], notification: SESSION_EVENT_NOTIFICATION, events: ["compactionStart", "compactionEnd"] },
|
|
418
461
|
import: { supported: false },
|
|
419
462
|
tree: { supported: true, method: SESSION_METHODS[8], labels: true, labelTimestamps: true, navigate: true, summarize: true, shape: "kward-tree-items-v1" },
|
|
420
|
-
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
|
|
421
471
|
},
|
|
422
472
|
pluginChats: {
|
|
423
473
|
supported: @plugin_chat_manager.supported_types.any?,
|
|
@@ -425,7 +475,7 @@ module Kward
|
|
|
425
475
|
notification: "pluginChat/event",
|
|
426
476
|
subscriptions: { supported: true, methods: PLUGIN_CHAT_METHODS.values_at(3, 4), requiredForLiveEvents: true },
|
|
427
477
|
attachments: { supported: true, method: PLUGIN_CHAT_METHODS[5], encoding: "base64", mimeTypes: SessionManager::RPC_IMAGE_MIME_TYPES, maxBytes: SessionManager::RPC_ATTACHMENT_MAX_BYTES },
|
|
428
|
-
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 }
|
|
429
479
|
},
|
|
430
480
|
turns: {
|
|
431
481
|
mode: "async",
|
|
@@ -481,7 +531,7 @@ module Kward
|
|
|
481
531
|
methods: MODEL_METHODS,
|
|
482
532
|
fields: ["provider", "id", "name", "reasoning", "reasoningEffort", "contextWindow"],
|
|
483
533
|
scopedModels: false,
|
|
484
|
-
|
|
534
|
+
refresh: { supported: true, method: MODEL_METHODS[4], providerParameter: true }
|
|
485
535
|
},
|
|
486
536
|
runtime: {
|
|
487
537
|
supported: true,
|
|
@@ -510,16 +560,25 @@ module Kward
|
|
|
510
560
|
},
|
|
511
561
|
auth: {
|
|
512
562
|
supported: true,
|
|
513
|
-
providerFormat: "kward-auth-
|
|
563
|
+
providerFormat: "kward-auth-v2",
|
|
514
564
|
methods: AUTH_METHODS,
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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,
|
|
518
576
|
logout: true
|
|
519
577
|
},
|
|
520
578
|
memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: MEMORY_METHODS },
|
|
521
579
|
stability: { protocol: "stable", compatibility: "additive-fields-unless-protocol-version-changes", experimentalCapabilities: [] },
|
|
522
580
|
commands: { supported: true, methods: COMMAND_METHODS, method: COMMAND_METHODS[0], runMethod: COMMAND_METHODS[1], sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
|
|
581
|
+
skillCapture: { supported: true, methods: SKILL_CAPTURE_METHODS, destination: "personal", source: "savedSessionActiveLeaf", reviewRequired: true, overwrite: "explicit", autoActivate: false },
|
|
523
582
|
mcp: {
|
|
524
583
|
supported: true,
|
|
525
584
|
transport: "stdio",
|
|
@@ -556,7 +615,8 @@ module Kward
|
|
|
556
615
|
workspaceMutationGuard: "none",
|
|
557
616
|
toolApproval: { supported: true, defaultMode: "none", modes: ["none", "ask"], requestNotification: TOOL_APPROVAL_NOTIFICATION, answerMethod: TOOL_APPROVAL_METHODS.first },
|
|
558
617
|
canRunShell: true,
|
|
559
|
-
canWriteFiles: true
|
|
618
|
+
canWriteFiles: true,
|
|
619
|
+
sandbox: sandbox_capabilities
|
|
560
620
|
},
|
|
561
621
|
export: { supported: true, formats: ["markdown", "html"], defaultFormat: "markdown" },
|
|
562
622
|
logging: {
|
|
@@ -576,6 +636,30 @@ module Kward
|
|
|
576
636
|
}
|
|
577
637
|
end
|
|
578
638
|
|
|
639
|
+
def sandbox_capabilities
|
|
640
|
+
config = @config_manager.read(redacted: false)
|
|
641
|
+
policy = ConfigFiles.sandbox_policy(Dir.pwd, config)
|
|
642
|
+
runner = Sandbox::RunnerFactory.build(policy)
|
|
643
|
+
runner.capabilities.to_h.merge(
|
|
644
|
+
mode: policy.mode,
|
|
645
|
+
network: policy.network,
|
|
646
|
+
scope: "shellCommandsOnly",
|
|
647
|
+
sessionPinning: false,
|
|
648
|
+
oneTimeElevation: false
|
|
649
|
+
)
|
|
650
|
+
rescue ArgumentError => error
|
|
651
|
+
{
|
|
652
|
+
available: false,
|
|
653
|
+
filesystemEnforced: false,
|
|
654
|
+
childNetworkEnforced: false,
|
|
655
|
+
backend: "invalidConfiguration",
|
|
656
|
+
reason: error.message,
|
|
657
|
+
scope: "shellCommandsOnly",
|
|
658
|
+
sessionPinning: false,
|
|
659
|
+
oneTimeElevation: false
|
|
660
|
+
}
|
|
661
|
+
end
|
|
662
|
+
|
|
579
663
|
def workspace_info(root)
|
|
580
664
|
root = @session_manager.validate_workspace_root(root)
|
|
581
665
|
{ root: root, basename: File.basename(root), writable: File.writable?(root) }
|
|
@@ -737,17 +821,21 @@ module Kward
|
|
|
737
821
|
end
|
|
738
822
|
|
|
739
823
|
def auth_login_with_api_key(params)
|
|
740
|
-
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"])
|
|
741
825
|
@session_manager.refresh_client_config
|
|
742
826
|
result
|
|
743
827
|
end
|
|
744
828
|
|
|
745
829
|
def auth_logout_provider(params)
|
|
746
|
-
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"])
|
|
747
831
|
@session_manager.refresh_client_config
|
|
748
832
|
result
|
|
749
833
|
end
|
|
750
834
|
|
|
835
|
+
def models_refresh(params)
|
|
836
|
+
{ models: @session_manager.refresh_models(provider: params["provider"]) }
|
|
837
|
+
end
|
|
838
|
+
|
|
751
839
|
def models_current
|
|
752
840
|
@session_manager.current_model
|
|
753
841
|
end
|
|
@@ -797,7 +885,7 @@ module Kward
|
|
|
797
885
|
end
|
|
798
886
|
|
|
799
887
|
def configured_workspace(root = Dir.pwd)
|
|
800
|
-
|
|
888
|
+
WorkspaceFactory.build(root: root, guardrails: workspace_guardrails_enabled?, config: @config_manager.read(redacted: false))
|
|
801
889
|
end
|
|
802
890
|
|
|
803
891
|
def workspace_guardrails_enabled?
|