kward 0.72.0 → 0.73.1
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 +30 -0
- data/CHANGELOG.md +59 -0
- data/Gemfile.lock +2 -2
- data/doc/configuration.md +1 -1
- data/doc/editor.md +23 -2
- data/doc/git.md +1 -0
- data/doc/rpc.md +2 -2
- data/doc/shell.md +56 -10
- data/doc/usage.md +27 -1
- data/lib/kward/ansi.rb +62 -23
- data/lib/kward/cli/plugins.rb +1 -1
- data/lib/kward/cli/rendering.rb +4 -1
- data/lib/kward/cli/runtime_helpers.rb +141 -7
- data/lib/kward/cli/settings.rb +0 -1
- data/lib/kward/cli/slash_commands.rb +213 -0
- data/lib/kward/cli/tabs.rb +34 -4
- data/lib/kward/cli/tool_summaries.rb +6 -0
- data/lib/kward/cli.rb +4 -12
- data/lib/kward/clipboard.rb +2 -3
- data/lib/kward/compactor.rb +7 -19
- data/lib/kward/config_files.rb +26 -4
- data/lib/kward/ekwsh.rb +239 -42
- data/lib/kward/image_attachments.rb +3 -1
- data/lib/kward/interactive_pty_runner.rb +151 -0
- data/lib/kward/local_command_runner.rb +155 -0
- data/lib/kward/local_pty_command_runner.rb +171 -0
- data/lib/kward/model/context_usage.rb +2 -2
- data/lib/kward/model/payloads.rb +2 -5
- data/lib/kward/prompt_history.rb +5 -3
- data/lib/kward/prompt_interface/editor/auto_indent.rb +5 -4
- data/lib/kward/prompt_interface/editor/controller.rb +262 -62
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +21 -21
- data/lib/kward/prompt_interface/editor/modes/modern.rb +38 -37
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +23 -173
- data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +6 -5
- data/lib/kward/prompt_interface/editor/state.rb +28 -6
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +5 -3
- data/lib/kward/prompt_interface/git_prompt.rb +12 -23
- data/lib/kward/prompt_interface/interactive/controller.rb +1 -1
- data/lib/kward/prompt_interface/key_handler.rb +93 -51
- data/lib/kward/prompt_interface/question_prompt.rb +1 -6
- data/lib/kward/prompt_interface/screen.rb +3 -3
- data/lib/kward/prompt_interface/selection_prompt.rb +12 -6
- data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
- data/lib/kward/prompt_interface.rb +87 -221
- data/lib/kward/prompts/commands.rb +4 -0
- data/lib/kward/rpc/memory_methods.rb +83 -0
- data/lib/kward/rpc/server.rb +130 -83
- data/lib/kward/rpc/session_manager.rb +10 -74
- data/lib/kward/rpc/tool_metadata.rb +11 -0
- data/lib/kward/rpc/transcript_normalizer.rb +4 -39
- data/lib/kward/scratchpad_runner.rb +56 -0
- data/lib/kward/session_diff.rb +20 -3
- data/lib/kward/session_naming.rb +11 -0
- data/lib/kward/terminal_keys.rb +84 -0
- data/lib/kward/terminal_sequences.rb +42 -0
- data/lib/kward/tools/context_for_task.rb +2 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +25 -0
- data/lib/kward/workers/job.rb +99 -0
- data/lib/kward/workers/queue_runner.rb +166 -0
- data/lib/kward/workers/queue_store.rb +112 -0
- data/lib/kward/workers.rb +3 -0
- data/lib/kward/workspace.rb +15 -63
- data/templates/default/fulldoc/html/css/kward.css +33 -0
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -0
- data/templates/default/layout/html/layout.erb +19 -32
- metadata +15 -1
data/lib/kward/rpc/server.rb
CHANGED
|
@@ -44,13 +44,20 @@ module Kward
|
|
|
44
44
|
invalid_params: -32_602,
|
|
45
45
|
internal_error: -32_603
|
|
46
46
|
}.freeze
|
|
47
|
+
PROTOCOL_METHODS = ["initialize", "shutdown"].freeze
|
|
48
|
+
WORKSPACE_METHODS = ["workspace/validate", "workspace/info"].freeze
|
|
49
|
+
TOOL_METHODS = ["tools/list"].freeze
|
|
50
|
+
PROMPT_METHODS = ["prompts/list", "prompts/expand"].freeze
|
|
47
51
|
SESSION_METHODS = [
|
|
48
52
|
"sessions/create", "sessions/resume", "sessions/list", "sessions/rename",
|
|
49
53
|
"sessions/clone", "sessions/compact", "sessions/forkMessages", "sessions/fork",
|
|
50
54
|
"sessions/tree", "sessions/tree/setLabel", "sessions/tree/navigate",
|
|
51
55
|
"sessions/export", "sessions/delete", "sessions/close", "sessions/transcript"
|
|
52
56
|
].freeze
|
|
57
|
+
TURN_METHODS = ["turns/start", "turns/cancel", "turns/status", "turns/events"].freeze
|
|
53
58
|
MODEL_METHODS = ["models/list", "models/current", "models/set", "reasoning/set"].freeze
|
|
59
|
+
RUNTIME_METHODS = ["runtime/state", "runtime/stats"].freeze
|
|
60
|
+
RUNTIME_SETTING_METHODS = ["runtime/updateSetting", "runtime/reload"].freeze
|
|
54
61
|
AUTH_METHODS = [
|
|
55
62
|
"auth/status", "auth/providers", "auth/loginWithApiKey", "auth/logoutProvider",
|
|
56
63
|
"auth/loginWithOAuth", "auth/startOpenAILogin", "auth/submitOpenAICode", "auth/loginStatus"
|
|
@@ -62,6 +69,36 @@ module Kward
|
|
|
62
69
|
"memory/why", "memory/summarize"
|
|
63
70
|
].freeze
|
|
64
71
|
WORKER_METHODS = ["workers/list", "workers/show"].freeze
|
|
72
|
+
COMMAND_METHODS = ["commands/list", "commands/run"].freeze
|
|
73
|
+
STARTUP_RESOURCE_METHODS = ["resources/startup"].freeze
|
|
74
|
+
CONFIG_METHODS = ["config/read", "config/update"].freeze
|
|
75
|
+
LOGGING_METHODS = ["logging/stats", "logging/tokenCsv"].freeze
|
|
76
|
+
UI_METHODS = ["ui/answerQuestion"].freeze
|
|
77
|
+
SESSION_EVENT_NOTIFICATION = "session/event"
|
|
78
|
+
SESSION_UPDATED_NOTIFICATION = "session/updated"
|
|
79
|
+
TURN_EVENT_NOTIFICATION = "turn/event"
|
|
80
|
+
UI_QUESTION_NOTIFICATION = "ui/question"
|
|
81
|
+
UI_FOOTER_NOTIFICATION = "ui/footer"
|
|
82
|
+
METHOD_GROUPS = {
|
|
83
|
+
protocol: PROTOCOL_METHODS,
|
|
84
|
+
workspace: WORKSPACE_METHODS,
|
|
85
|
+
tools: TOOL_METHODS,
|
|
86
|
+
prompts: PROMPT_METHODS,
|
|
87
|
+
sessions: SESSION_METHODS,
|
|
88
|
+
turns: TURN_METHODS,
|
|
89
|
+
models: MODEL_METHODS,
|
|
90
|
+
runtime: RUNTIME_METHODS,
|
|
91
|
+
runtime_settings: RUNTIME_SETTING_METHODS,
|
|
92
|
+
auth: AUTH_METHODS,
|
|
93
|
+
memory: MEMORY_METHODS,
|
|
94
|
+
workers: WORKER_METHODS,
|
|
95
|
+
commands: COMMAND_METHODS,
|
|
96
|
+
startup_resources: STARTUP_RESOURCE_METHODS,
|
|
97
|
+
config: CONFIG_METHODS,
|
|
98
|
+
logging: LOGGING_METHODS,
|
|
99
|
+
ui: UI_METHODS
|
|
100
|
+
}.freeze
|
|
101
|
+
RPC_METHODS = METHOD_GROUPS.values.flatten.freeze
|
|
65
102
|
|
|
66
103
|
# Creates the RPC server and its stateful managers.
|
|
67
104
|
def initialize(input: $stdin, output: $stdout, error_output: $stderr, client: Client.new, experimental_workers: false)
|
|
@@ -154,145 +191,145 @@ module Kward
|
|
|
154
191
|
def dispatch(method, params)
|
|
155
192
|
params = stringify_keys(params || {})
|
|
156
193
|
case method
|
|
157
|
-
when
|
|
194
|
+
when PROTOCOL_METHODS[0]
|
|
158
195
|
initialize_result
|
|
159
|
-
when
|
|
196
|
+
when PROTOCOL_METHODS[1]
|
|
160
197
|
@shutdown = true
|
|
161
198
|
{ ok: true }
|
|
162
|
-
when
|
|
199
|
+
when WORKSPACE_METHODS[0]
|
|
163
200
|
{ root: @session_manager.validate_workspace_root(params["workspaceRoot"] || Dir.pwd) }
|
|
164
|
-
when
|
|
201
|
+
when WORKSPACE_METHODS[1]
|
|
165
202
|
workspace_info(params["workspaceRoot"] || Dir.pwd)
|
|
166
|
-
when
|
|
203
|
+
when TOOL_METHODS[0]
|
|
167
204
|
{ tools: ToolRegistry.new(workspace: configured_workspace).schemas }
|
|
168
|
-
when
|
|
205
|
+
when PROMPT_METHODS[0]
|
|
169
206
|
prompts_list
|
|
170
|
-
when
|
|
207
|
+
when PROMPT_METHODS[1]
|
|
171
208
|
prompts_expand(params)
|
|
172
|
-
when
|
|
209
|
+
when MODEL_METHODS[0]
|
|
173
210
|
models_list
|
|
174
|
-
when
|
|
211
|
+
when MODEL_METHODS[1]
|
|
175
212
|
models_current
|
|
176
|
-
when
|
|
213
|
+
when MODEL_METHODS[2]
|
|
177
214
|
models_set(params)
|
|
178
|
-
when
|
|
215
|
+
when MODEL_METHODS[3]
|
|
179
216
|
reasoning_set(params)
|
|
180
|
-
when
|
|
217
|
+
when RUNTIME_METHODS[0]
|
|
181
218
|
@session_manager.runtime_state(session_id: params.fetch("sessionId"))
|
|
182
|
-
when
|
|
219
|
+
when RUNTIME_METHODS[1]
|
|
183
220
|
@session_manager.runtime_stats(session_id: params.fetch("sessionId"))
|
|
184
|
-
when
|
|
221
|
+
when RUNTIME_SETTING_METHODS[0]
|
|
185
222
|
runtime_update_setting(params)
|
|
186
|
-
when
|
|
223
|
+
when RUNTIME_SETTING_METHODS[1]
|
|
187
224
|
runtime_reload(params)
|
|
188
|
-
when
|
|
225
|
+
when COMMAND_METHODS[0]
|
|
189
226
|
commands_list(params)
|
|
190
|
-
when
|
|
227
|
+
when COMMAND_METHODS[1]
|
|
191
228
|
commands_run(params)
|
|
192
|
-
when
|
|
229
|
+
when STARTUP_RESOURCE_METHODS[0]
|
|
193
230
|
startup_resources(params)
|
|
194
|
-
when
|
|
231
|
+
when CONFIG_METHODS[0]
|
|
195
232
|
{ path: @config_manager.config_path, config: @config_manager.read(redacted: params.fetch("redacted", true)) }
|
|
196
|
-
when
|
|
233
|
+
when CONFIG_METHODS[1]
|
|
197
234
|
config_update(params)
|
|
198
|
-
when
|
|
235
|
+
when LOGGING_METHODS[0]
|
|
199
236
|
logging_stats(params)
|
|
200
|
-
when
|
|
237
|
+
when LOGGING_METHODS[1]
|
|
201
238
|
logging_token_csv(params)
|
|
202
|
-
when
|
|
239
|
+
when MEMORY_METHODS[0]
|
|
203
240
|
@session_manager.memory_status
|
|
204
|
-
when
|
|
241
|
+
when MEMORY_METHODS[1]
|
|
205
242
|
@session_manager.memory_enable
|
|
206
|
-
when
|
|
243
|
+
when MEMORY_METHODS[2]
|
|
207
244
|
@session_manager.memory_disable
|
|
208
|
-
when
|
|
245
|
+
when MEMORY_METHODS[3]
|
|
209
246
|
@session_manager.memory_auto_summary_enable
|
|
210
|
-
when
|
|
247
|
+
when MEMORY_METHODS[4]
|
|
211
248
|
@session_manager.memory_auto_summary_disable
|
|
212
|
-
when
|
|
249
|
+
when MEMORY_METHODS[5]
|
|
213
250
|
@session_manager.memory_list(include_inactive: params["includeInactive"] || false, workspace_root: params["workspaceRoot"] || Dir.pwd)
|
|
214
|
-
when
|
|
251
|
+
when MEMORY_METHODS[6]
|
|
215
252
|
@session_manager.memory_add(text: params.fetch("text"), scope: params["scope"], tags: params["tags"] || [])
|
|
216
|
-
when
|
|
253
|
+
when MEMORY_METHODS[7]
|
|
217
254
|
@session_manager.memory_add_core(text: params.fetch("text"), scope: params["scope"], tags: params["tags"] || [])
|
|
218
|
-
when
|
|
255
|
+
when MEMORY_METHODS[8]
|
|
219
256
|
@session_manager.memory_forget(id: params.fetch("id"))
|
|
220
|
-
when
|
|
257
|
+
when MEMORY_METHODS[9]
|
|
221
258
|
@session_manager.memory_promote(id: params.fetch("id"))
|
|
222
|
-
when
|
|
259
|
+
when MEMORY_METHODS[10]
|
|
223
260
|
@session_manager.memory_relax(id: params.fetch("id"), workspace_root: params["workspaceRoot"] || Dir.pwd)
|
|
224
|
-
when
|
|
261
|
+
when MEMORY_METHODS[11]
|
|
225
262
|
@session_manager.memory_inspect
|
|
226
|
-
when
|
|
263
|
+
when MEMORY_METHODS[12]
|
|
227
264
|
@session_manager.memory_why(session_id: params["sessionId"])
|
|
228
|
-
when
|
|
265
|
+
when MEMORY_METHODS[13]
|
|
229
266
|
@session_manager.memory_summarize(session_id: params.fetch("sessionId"))
|
|
230
|
-
when
|
|
267
|
+
when WORKER_METHODS[0]
|
|
231
268
|
require_experimental_workers!
|
|
232
269
|
workers_list(params)
|
|
233
|
-
when
|
|
270
|
+
when WORKER_METHODS[1]
|
|
234
271
|
require_experimental_workers!
|
|
235
272
|
workers_show(params)
|
|
236
|
-
when
|
|
273
|
+
when AUTH_METHODS[0]
|
|
237
274
|
@auth_manager.status
|
|
238
|
-
when
|
|
275
|
+
when AUTH_METHODS[1]
|
|
239
276
|
@auth_manager.providers
|
|
240
|
-
when
|
|
277
|
+
when AUTH_METHODS[2]
|
|
241
278
|
auth_login_with_api_key(params)
|
|
242
|
-
when
|
|
279
|
+
when AUTH_METHODS[3]
|
|
243
280
|
auth_logout_provider(params)
|
|
244
|
-
when
|
|
281
|
+
when AUTH_METHODS[4]
|
|
245
282
|
@auth_manager.login_with_oauth(provider_id: params.fetch("providerId"), timeout_seconds: params["timeoutSeconds"] || 120)
|
|
246
|
-
when
|
|
283
|
+
when AUTH_METHODS[5]
|
|
247
284
|
@auth_manager.start_openai_login(timeout_seconds: params["timeoutSeconds"] || 120)
|
|
248
|
-
when
|
|
285
|
+
when AUTH_METHODS[6]
|
|
249
286
|
@auth_manager.submit_openai_code(login_id: params.fetch("loginId"), code: params.fetch("code"))
|
|
250
|
-
when
|
|
287
|
+
when AUTH_METHODS[7]
|
|
251
288
|
@auth_manager.login_status(login_id: params.fetch("loginId"))
|
|
252
|
-
when
|
|
289
|
+
when SESSION_METHODS[0]
|
|
253
290
|
@session_manager.create_session(workspace_root: params["workspaceRoot"] || Dir.pwd, name: params["name"], resume_last: params["resumeLast"] != false)
|
|
254
|
-
when
|
|
291
|
+
when SESSION_METHODS[1]
|
|
255
292
|
@session_manager.resume_session(path: params.fetch("path"), workspace_root: params["workspaceRoot"])
|
|
256
|
-
when
|
|
293
|
+
when SESSION_METHODS[2]
|
|
257
294
|
{ sessions: @session_manager.list_sessions(workspace_root: params["workspaceRoot"] || Dir.pwd, limit: params["limit"], current_session_path: params["currentSessionPath"]) }
|
|
258
|
-
when
|
|
295
|
+
when SESSION_METHODS[3]
|
|
259
296
|
@session_manager.rename_session(session_id: params.fetch("sessionId"), name: params["name"])
|
|
260
|
-
when
|
|
297
|
+
when SESSION_METHODS[4]
|
|
261
298
|
@session_manager.clone_session(session_id: params.fetch("sessionId"))
|
|
262
|
-
when
|
|
299
|
+
when SESSION_METHODS[5]
|
|
263
300
|
@session_manager.compact_session(session_id: params.fetch("sessionId"), custom_instructions: params["customInstructions"] || "")
|
|
264
|
-
when
|
|
301
|
+
when SESSION_METHODS[6]
|
|
265
302
|
@session_manager.fork_messages(session_id: params.fetch("sessionId"))
|
|
266
|
-
when
|
|
303
|
+
when SESSION_METHODS[7]
|
|
267
304
|
@session_manager.fork_session(session_id: params.fetch("sessionId"), entry_id: params.fetch("entryId"))
|
|
268
|
-
when
|
|
305
|
+
when SESSION_METHODS[8]
|
|
269
306
|
@session_manager.session_tree(session_id: params.fetch("sessionId"))
|
|
270
|
-
when
|
|
307
|
+
when SESSION_METHODS[9]
|
|
271
308
|
@session_manager.set_tree_label(session_id: params.fetch("sessionId"), entry_id: params.fetch("entryId"), label: params["label"])
|
|
272
|
-
when
|
|
309
|
+
when SESSION_METHODS[10]
|
|
273
310
|
@session_manager.navigate_tree(session_id: params.fetch("sessionId"), entry_id: params.fetch("entryId"), summarize: params["summarize"], custom_instructions: params["customInstructions"])
|
|
274
|
-
when
|
|
311
|
+
when SESSION_METHODS[11]
|
|
275
312
|
@session_manager.export_session(session_id: params.fetch("sessionId"), path: params["path"], format: params["format"])
|
|
276
|
-
when
|
|
313
|
+
when SESSION_METHODS[12]
|
|
277
314
|
@session_manager.delete_session(session_id: params.fetch("sessionId"))
|
|
278
|
-
when
|
|
315
|
+
when SESSION_METHODS[13]
|
|
279
316
|
@session_manager.close_session(session_id: params.fetch("sessionId"))
|
|
280
|
-
when
|
|
317
|
+
when SESSION_METHODS[14]
|
|
281
318
|
@session_manager.transcript(session_id: params.fetch("sessionId"))
|
|
282
|
-
when
|
|
319
|
+
when TURN_METHODS[0]
|
|
283
320
|
@session_manager.start_turn(
|
|
284
321
|
session_id: params.fetch("sessionId"),
|
|
285
322
|
input: params.fetch("input"),
|
|
286
323
|
streaming_behavior: params["streamingBehavior"],
|
|
287
324
|
attachments: params["attachments"] || []
|
|
288
325
|
)
|
|
289
|
-
when
|
|
326
|
+
when TURN_METHODS[1]
|
|
290
327
|
@session_manager.cancel_turn(turn_id: params.fetch("turnId"))
|
|
291
|
-
when
|
|
328
|
+
when TURN_METHODS[2]
|
|
292
329
|
@session_manager.turn_status(turn_id: params.fetch("turnId"))
|
|
293
|
-
when
|
|
330
|
+
when TURN_METHODS[3]
|
|
294
331
|
@session_manager.turn_events(turn_id: params.fetch("turnId"), after_sequence: params["afterSequence"] || 0)
|
|
295
|
-
when
|
|
332
|
+
when UI_METHODS[0]
|
|
296
333
|
@session_manager.answer_question(session_id: params.fetch("sessionId"), question_request_id: params.fetch("questionRequestId"), answers: params.fetch("answers"))
|
|
297
334
|
else
|
|
298
335
|
raise NoMethodError, method
|
|
@@ -324,13 +361,13 @@ module Kward
|
|
|
324
361
|
mode: "explicit",
|
|
325
362
|
persistence: "jsonl",
|
|
326
363
|
methods: SESSION_METHODS,
|
|
327
|
-
startupResume: { supported: true, method:
|
|
364
|
+
startupResume: { supported: true, method: SESSION_METHODS[0], parameter: "resumeLast", default: session_auto_resume_enabled?, immediateTranscript: true, sessionActivePersonaLabel: true },
|
|
328
365
|
list: { supported: true, source: "rpc", ancestry: true, treeFields: true },
|
|
329
|
-
fork: { supported: true, methods:
|
|
330
|
-
compact: { supported: true, method:
|
|
366
|
+
fork: { supported: true, methods: SESSION_METHODS.values_at(6, 7), entryIdFormat: "entry-id", selectedMessage: "excludedFromForkComposerTextReturned" },
|
|
367
|
+
compact: { supported: true, method: SESSION_METHODS[5], notification: SESSION_EVENT_NOTIFICATION, events: ["compactionStart", "compactionEnd"] },
|
|
331
368
|
import: { supported: false },
|
|
332
|
-
tree: { supported: true, method:
|
|
333
|
-
updates: { supported: false, notification:
|
|
369
|
+
tree: { supported: true, method: SESSION_METHODS[8], labels: true, labelTimestamps: true, navigate: true, summarize: true, shape: "tauren-tree-items-v1" },
|
|
370
|
+
updates: { supported: false, notification: SESSION_UPDATED_NOTIFICATION }
|
|
334
371
|
},
|
|
335
372
|
turns: {
|
|
336
373
|
mode: "async",
|
|
@@ -349,19 +386,19 @@ module Kward
|
|
|
349
386
|
eventReplay: { behavior: "recent-in-memory", persisted: false, limit: SessionManager::RECENT_EVENT_LIMIT }
|
|
350
387
|
},
|
|
351
388
|
events: {
|
|
352
|
-
notification:
|
|
389
|
+
notification: TURN_EVENT_NOTIFICATION,
|
|
353
390
|
assistantText: "assistantDelta",
|
|
354
391
|
reasoning: { start: false, delta: true, end: false },
|
|
355
392
|
modelRetry: { supported: true, event: "modelRetry" },
|
|
356
393
|
steering: { supported: @session_manager.in_flight_steer_supported?, event: "turnSteered", mode: @session_manager.in_flight_steer_supported? ? "native" : "unsupported" },
|
|
357
|
-
tools: { call: true, update: false, result: true, normalizedMetadata: true, diffs: true, changedFiles:
|
|
394
|
+
tools: { call: true, update: false, result: true, normalizedMetadata: true, diffs: true, changedFiles: true, workspaceGuardrails: workspace_guardrails_enabled?, focusedContext: true, contextBudgetStats: true },
|
|
358
395
|
errors: true,
|
|
359
396
|
sessionUpdates: false
|
|
360
397
|
},
|
|
361
398
|
attachments: {
|
|
362
399
|
input: {
|
|
363
400
|
supported: true,
|
|
364
|
-
method:
|
|
401
|
+
method: TURN_METHODS[0],
|
|
365
402
|
encoding: "base64",
|
|
366
403
|
mimeTypes: SessionManager::RPC_IMAGE_MIME_TYPES,
|
|
367
404
|
maxBytes: SessionManager::RPC_ATTACHMENT_MAX_BYTES
|
|
@@ -376,13 +413,13 @@ module Kward
|
|
|
376
413
|
},
|
|
377
414
|
runtime: {
|
|
378
415
|
supported: true,
|
|
379
|
-
methods:
|
|
416
|
+
methods: RUNTIME_METHODS,
|
|
380
417
|
state: { supported: true },
|
|
381
418
|
stats: { messageCounts: true, tokens: false, cost: false, contextUsage: true, contextUsageEstimated: true }
|
|
382
419
|
},
|
|
383
420
|
runtimeSettings: {
|
|
384
421
|
supported: true,
|
|
385
|
-
methods:
|
|
422
|
+
methods: RUNTIME_SETTING_METHODS,
|
|
386
423
|
settings: ["defaultModel", "defaultThinkingLevel"]
|
|
387
424
|
},
|
|
388
425
|
auth: {
|
|
@@ -396,18 +433,19 @@ module Kward
|
|
|
396
433
|
},
|
|
397
434
|
memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: MEMORY_METHODS },
|
|
398
435
|
workers: workers_capability,
|
|
399
|
-
commands: { supported: true, methods:
|
|
400
|
-
startupResources: { supported: true, method:
|
|
436
|
+
commands: { supported: true, methods: COMMAND_METHODS, method: COMMAND_METHODS[0], runMethod: COMMAND_METHODS[1], sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
|
|
437
|
+
startupResources: { supported: true, method: STARTUP_RESOURCE_METHODS.first },
|
|
401
438
|
starterPack: { supported: false, reason: "cliOnlyInstallCommand" },
|
|
402
439
|
shell: { supported: false, reason: "interactiveTuiOnly" },
|
|
440
|
+
scratchpad: { supported: false, reason: "interactiveTuiOnly" },
|
|
403
441
|
extensionUi: {
|
|
404
|
-
question: { supported: true, notification:
|
|
442
|
+
question: { supported: true, notification: UI_QUESTION_NOTIFICATION, method: UI_METHODS.first, maxQuestions: 4, multiSelect: false, preview: false },
|
|
405
443
|
select: false,
|
|
406
444
|
confirm: false,
|
|
407
445
|
input: false,
|
|
408
446
|
editor: false,
|
|
409
447
|
widgets: false,
|
|
410
|
-
footer: { supported: true, notification:
|
|
448
|
+
footer: { supported: true, notification: UI_FOOTER_NOTIFICATION },
|
|
411
449
|
custom: false,
|
|
412
450
|
terminalInput: false
|
|
413
451
|
},
|
|
@@ -425,9 +463,9 @@ module Kward
|
|
|
425
463
|
logging: {
|
|
426
464
|
supported: true,
|
|
427
465
|
defaultEnabled: false,
|
|
428
|
-
methods:
|
|
429
|
-
stats: { supported: true, method:
|
|
430
|
-
usageCsv: { supported: true, method:
|
|
466
|
+
methods: LOGGING_METHODS,
|
|
467
|
+
stats: { supported: true, method: LOGGING_METHODS[0], defaultRange: TelemetryStats::DEFAULT_RANGE, units: %w[minutes hours days weeks months years] },
|
|
468
|
+
usageCsv: { supported: true, method: LOGGING_METHODS[1], defaultRange: TelemetryStats::DEFAULT_RANGE, buckets: %w[second minute hour day week month year] },
|
|
431
469
|
config: "logging",
|
|
432
470
|
envPrefix: "KWARD_LOGGING",
|
|
433
471
|
directory: File.join(ConfigFiles.config_dir, "logs"),
|
|
@@ -641,6 +679,15 @@ module Kward
|
|
|
641
679
|
end
|
|
642
680
|
|
|
643
681
|
def provider_model_from(value)
|
|
682
|
+
if value.is_a?(Hash)
|
|
683
|
+
provider = value["provider"] || value[:provider] || @session_manager.current_model[:provider]
|
|
684
|
+
model = value["model"] || value[:model] || value["id"] || value[:id]
|
|
685
|
+
model = model.to_s.strip
|
|
686
|
+
raise ArgumentError, "Model must be a non-empty string" if model.empty?
|
|
687
|
+
|
|
688
|
+
return [provider, model]
|
|
689
|
+
end
|
|
690
|
+
|
|
644
691
|
text = value.to_s.strip
|
|
645
692
|
raise ArgumentError, "Model must be a non-empty string" if text.empty?
|
|
646
693
|
|
|
@@ -18,6 +18,7 @@ require_relative "../model/model_info"
|
|
|
18
18
|
require_relative "../plugin_registry"
|
|
19
19
|
require_relative "../prompts/commands"
|
|
20
20
|
require_relative "../session_store"
|
|
21
|
+
require_relative "../session_naming"
|
|
21
22
|
require_relative "../session_trash"
|
|
22
23
|
require_relative "../steering"
|
|
23
24
|
require_relative "../tools/tool_call"
|
|
@@ -26,6 +27,7 @@ require_relative "../transcript_export"
|
|
|
26
27
|
require_relative "../workspace"
|
|
27
28
|
require_relative "attachment_normalizer"
|
|
28
29
|
require_relative "config_manager"
|
|
30
|
+
require_relative "memory_methods"
|
|
29
31
|
require_relative "prompt_bridge"
|
|
30
32
|
require_relative "runtime_payloads"
|
|
31
33
|
require_relative "session_metrics"
|
|
@@ -51,6 +53,8 @@ module Kward
|
|
|
51
53
|
# `ToolRegistry`. This class should coordinate those pieces rather than own
|
|
52
54
|
# their low-level mechanics.
|
|
53
55
|
class SessionManager
|
|
56
|
+
include MemoryMethods
|
|
57
|
+
|
|
54
58
|
RECENT_EVENT_LIMIT = 1_000
|
|
55
59
|
RPC_ATTACHMENT_MAX_BYTES = AttachmentNormalizer::MAX_BYTES
|
|
56
60
|
RPC_IMAGE_MIME_TYPES = AttachmentNormalizer::IMAGE_MIME_TYPES
|
|
@@ -391,78 +395,6 @@ module Kward
|
|
|
391
395
|
run_plugin_command(session_id: session_id, command: name, arguments: arguments)
|
|
392
396
|
end
|
|
393
397
|
|
|
394
|
-
def memory_manager
|
|
395
|
-
Memory::Manager.for_config_dir(@config_dir)
|
|
396
|
-
end
|
|
397
|
-
|
|
398
|
-
def memory_status
|
|
399
|
-
manager = memory_manager
|
|
400
|
-
{ enabled: manager.enabled?, autoSummary: manager.auto_summary_enabled?, paths: manager.paths }
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
def memory_enable
|
|
404
|
-
memory_manager.enable
|
|
405
|
-
{ enabled: true }
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
def memory_disable
|
|
409
|
-
memory_manager.disable
|
|
410
|
-
{ enabled: false }
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
def memory_auto_summary_enable
|
|
414
|
-
memory_manager.auto_summary_enable
|
|
415
|
-
{ autoSummary: true }
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
def memory_auto_summary_disable
|
|
419
|
-
memory_manager.auto_summary_disable
|
|
420
|
-
{ autoSummary: false }
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
def memory_list(include_inactive: false, workspace_root: Dir.pwd)
|
|
424
|
-
memory_manager.hierarchy(include_inactive: include_inactive, workspace_root: workspace_root)
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
def memory_add(text:, scope: nil, tags: [])
|
|
428
|
-
{ memory: memory_manager.add_soft(text, scope: scope || "global", tags: tags) }
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
def memory_add_core(text:, scope: nil, tags: [])
|
|
432
|
-
{ memory: memory_manager.add_core(text, scope: scope || "global", tags: tags) }
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
def memory_forget(id:)
|
|
436
|
-
{ forgotten: memory_manager.forget_memory(id) }
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
def memory_promote(id:)
|
|
440
|
-
{ memory: memory_manager.promote_memory(id) }
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
def memory_relax(id:, workspace_root: Dir.pwd)
|
|
444
|
-
{ memory: memory_manager.relax_core(id, workspace_root: workspace_root) }
|
|
445
|
-
end
|
|
446
|
-
|
|
447
|
-
def memory_inspect
|
|
448
|
-
memory_manager.inspect_memory
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
def memory_why(session_id: nil)
|
|
452
|
-
if session_id
|
|
453
|
-
rpc_session = fetch_session(session_id)
|
|
454
|
-
return rpc_session.conversation.last_memory_retrieval || memory_manager.explain_retrieval
|
|
455
|
-
end
|
|
456
|
-
memory_manager.explain_retrieval
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
def memory_summarize(session_id:)
|
|
460
|
-
rpc_session = fetch_session(session_id)
|
|
461
|
-
records = memory_manager.summarize_conversation(rpc_session.conversation, client: @client)
|
|
462
|
-
persist_memory_state(rpc_session)
|
|
463
|
-
{ memories: records }
|
|
464
|
-
end
|
|
465
|
-
|
|
466
398
|
def run_plugin_command(session_id:, command:, arguments: "")
|
|
467
399
|
rpc_session = fetch_session(session_id)
|
|
468
400
|
command = plugin_registry.command_for(command.to_s.delete_prefix("/")) || raise(ArgumentError, "Unknown plugin command: #{command}")
|
|
@@ -613,11 +545,15 @@ module Kward
|
|
|
613
545
|
end
|
|
614
546
|
|
|
615
547
|
def refresh_session_runtime_contexts
|
|
548
|
+
provider = current_model[:provider]
|
|
616
549
|
model = current_model_id
|
|
617
550
|
reasoning_effort = current_reasoning_effort
|
|
618
551
|
sessions = @mutex.synchronize { @sessions.values }
|
|
619
552
|
sessions.each do |rpc_session|
|
|
620
|
-
rpc_session.conversation
|
|
553
|
+
conversation = rpc_session.conversation
|
|
554
|
+
runtime_changed = [conversation.provider, conversation.model, conversation.reasoning_effort] != [provider, model, reasoning_effort]
|
|
555
|
+
conversation.update_runtime_context!(provider: provider, model: model, reasoning_effort: reasoning_effort)
|
|
556
|
+
conversation.persist_runtime_context! if runtime_changed
|
|
621
557
|
end
|
|
622
558
|
end
|
|
623
559
|
|
|
@@ -1036,7 +972,7 @@ module Kward
|
|
|
1036
972
|
end
|
|
1037
973
|
|
|
1038
974
|
def default_session_name(input)
|
|
1039
|
-
|
|
975
|
+
SessionNaming.default_name(input)
|
|
1040
976
|
end
|
|
1041
977
|
|
|
1042
978
|
def turn_display_input(turn)
|
|
@@ -75,6 +75,17 @@ module Kward
|
|
|
75
75
|
index ? text.to_s[index..] : nil
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
def changed_files_from_result(text, matching_call = nil)
|
|
79
|
+
path = matching_call&.dig(:arguments, :path) || matching_call&.dig(:arguments, "path")
|
|
80
|
+
return [path] if path
|
|
81
|
+
|
|
82
|
+
if (match = text.to_s.match(/\A(?:Wrote \d+ bytes to|Edited)\s+([^:\n]+)/))
|
|
83
|
+
[match[1].strip]
|
|
84
|
+
else
|
|
85
|
+
[]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
78
89
|
def error_result?(text)
|
|
79
90
|
text.to_s.start_with?("Error:", "Declined:", "Cancelled.")
|
|
80
91
|
end
|
|
@@ -106,7 +106,7 @@ module Kward
|
|
|
106
106
|
type: "toolCall",
|
|
107
107
|
id: ToolCall.id(tool_call),
|
|
108
108
|
name: normalize_tool_name(raw_name) || raw_name,
|
|
109
|
-
arguments:
|
|
109
|
+
arguments: ToolMetadata.normalize_tool_args(raw_name, ToolCall.parse_arguments(ToolCall.raw_arguments(tool_call)))
|
|
110
110
|
}.compact
|
|
111
111
|
end
|
|
112
112
|
|
|
@@ -235,7 +235,7 @@ module Kward
|
|
|
235
235
|
type: "toolCall",
|
|
236
236
|
id: ToolCall.value(part, :id),
|
|
237
237
|
name: normalize_tool_name(raw_name) || raw_name,
|
|
238
|
-
arguments:
|
|
238
|
+
arguments: ToolMetadata.normalize_tool_args(raw_name, ToolCall.parse_arguments(arguments))
|
|
239
239
|
}.compact
|
|
240
240
|
end
|
|
241
241
|
|
|
@@ -244,10 +244,10 @@ module Kward
|
|
|
244
244
|
details = explicit_details.is_a?(Hash) ? safe_details(explicit_details) : {}
|
|
245
245
|
text = content_text(content)
|
|
246
246
|
|
|
247
|
-
diff = details[:diff] || details["diff"] || extract_unified_diff(text)
|
|
247
|
+
diff = details[:diff] || details["diff"] || ToolMetadata.extract_unified_diff(text)
|
|
248
248
|
details[:diff] = diff if diff
|
|
249
249
|
|
|
250
|
-
changed_files = details[:changedFiles] || details["changedFiles"] || changed_files_from_result(text, matching_call)
|
|
250
|
+
changed_files = details[:changedFiles] || details["changedFiles"] || ToolMetadata.changed_files_from_result(text, matching_call)
|
|
251
251
|
details[:changedFiles] = changed_files if changed_files && !changed_files.empty?
|
|
252
252
|
|
|
253
253
|
details
|
|
@@ -262,21 +262,6 @@ module Kward
|
|
|
262
262
|
allowed
|
|
263
263
|
end
|
|
264
264
|
|
|
265
|
-
def extract_unified_diff(text)
|
|
266
|
-
ToolMetadata.extract_unified_diff(text)
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def changed_files_from_result(text, matching_call)
|
|
270
|
-
path = matching_call&.dig(:arguments, :path) || matching_call&.dig(:arguments, "path")
|
|
271
|
-
return [path] if path
|
|
272
|
-
|
|
273
|
-
if (match = text.match(/\A(?:Wrote \d+ bytes to|Edited)\s+([^:\n]+)/))
|
|
274
|
-
[match[1].strip]
|
|
275
|
-
else
|
|
276
|
-
[]
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
265
|
def error_tool_result?(message, content)
|
|
281
266
|
return ToolCall.value(message, :isError) if has_key?(message, :isError)
|
|
282
267
|
return ToolCall.value(message, :is_error) if has_key?(message, :is_error)
|
|
@@ -299,26 +284,6 @@ module Kward
|
|
|
299
284
|
ToolMetadata.normalize_tool_name(name)
|
|
300
285
|
end
|
|
301
286
|
|
|
302
|
-
def normalize_tool_arguments(name, arguments)
|
|
303
|
-
args = ToolCall.parse_arguments(arguments)
|
|
304
|
-
case name.to_s
|
|
305
|
-
when "edit_file", "edit"
|
|
306
|
-
ToolMetadata.normalize_tool_args(name, args)
|
|
307
|
-
when "run_shell_command", "bash"
|
|
308
|
-
normalize_bash_args(args)
|
|
309
|
-
else
|
|
310
|
-
ToolCall.camelize_args(args)
|
|
311
|
-
end
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
def normalize_bash_args(args)
|
|
315
|
-
normalized = ToolCall.camelize_args(args)
|
|
316
|
-
timeout = ToolCall.value(args, :timeoutSeconds) || ToolCall.value(args, :timeout_seconds)
|
|
317
|
-
normalized[:timeoutSeconds] = timeout if timeout
|
|
318
|
-
normalized.delete(:timeout_seconds)
|
|
319
|
-
normalized
|
|
320
|
-
end
|
|
321
|
-
|
|
322
287
|
def normalize_mime_type(mime_type)
|
|
323
288
|
mime_type.to_s.downcase.sub("image/jpg", "image/jpeg")
|
|
324
289
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "rbconfig"
|
|
3
|
+
require "tempfile"
|
|
4
|
+
|
|
5
|
+
# Namespace for the Kward CLI agent runtime.
|
|
6
|
+
module Kward
|
|
7
|
+
# Executes scratchpad buffers and returns transformed buffer content.
|
|
8
|
+
module ScratchpadRunner
|
|
9
|
+
RUBY_END_MARKER_PATTERN = /^__END__\n?/.freeze
|
|
10
|
+
|
|
11
|
+
Result = Struct.new(:buffer, :exit_status, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def run(language, content)
|
|
16
|
+
case language&.to_sym
|
|
17
|
+
when :ruby
|
|
18
|
+
run_ruby(content)
|
|
19
|
+
else
|
|
20
|
+
raise ArgumentError, "Scratchpad language #{language.inspect} is not runnable"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run_ruby(content)
|
|
25
|
+
content = content.to_s
|
|
26
|
+
output, status = capture_ruby_output(content)
|
|
27
|
+
Result.new(buffer: ruby_buffer_with_output(content, output, status.exitstatus), exit_status: status.exitstatus)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def capture_ruby_output(content)
|
|
31
|
+
Tempfile.create(["kward-scratchpad", ".rb"]) do |file|
|
|
32
|
+
file.write(content)
|
|
33
|
+
file.flush
|
|
34
|
+
Open3.capture2e(RbConfig.ruby, file.path)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ruby_buffer_with_output(content, output, exit_status)
|
|
39
|
+
output = output.to_s
|
|
40
|
+
output += "\n" unless output.empty? || output.end_with?("\n")
|
|
41
|
+
output += "[exit status: #{exit_status}]\n" unless exit_status.to_i.zero?
|
|
42
|
+
|
|
43
|
+
if (match = content.match(RUBY_END_MARKER_PATTERN))
|
|
44
|
+
"#{content[0...match.begin(0)]}__END__\n#{output}"
|
|
45
|
+
else
|
|
46
|
+
"#{content}#{ruby_end_separator(content)}__END__\n#{output}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def ruby_end_separator(content)
|
|
51
|
+
return "" if content.empty?
|
|
52
|
+
|
|
53
|
+
content.end_with?("\n") ? "\n" : "\n\n"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|