kward 0.68.0 → 0.69.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/pages.yml +48 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +34 -0
- data/Gemfile.lock +8 -2
- data/README.md +32 -25
- data/Rakefile +14 -1
- data/doc/authentication.md +74 -56
- data/doc/code-search.md +55 -28
- data/doc/configuration.md +18 -0
- data/doc/extensibility.md +89 -128
- data/doc/getting-started.md +52 -54
- data/doc/memory.md +51 -118
- data/doc/personas.md +417 -0
- data/doc/plugins.md +55 -97
- data/doc/releasing.md +3 -1
- data/doc/rpc.md +1 -1
- data/doc/usage.md +125 -144
- data/doc/web-search.md +80 -14
- data/exe/kward +2 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +10 -3
- data/lib/kward/cli/compaction.rb +3 -3
- data/lib/kward/cli/interactive_turn.rb +3 -1
- data/lib/kward/cli/memory_commands.rb +16 -16
- data/lib/kward/cli/plugins.rb +3 -3
- data/lib/kward/cli/prompt_interface.rb +15 -13
- data/lib/kward/cli/rendering.rb +35 -46
- data/lib/kward/cli/runtime_helpers.rb +13 -2
- data/lib/kward/cli/sessions.rb +21 -21
- data/lib/kward/cli/settings.rb +49 -43
- data/lib/kward/cli/slash_commands.rb +6 -4
- data/lib/kward/cli/stats.rb +2 -2
- data/lib/kward/cli/sysprompt.rb +57 -0
- data/lib/kward/cli/tool_summaries.rb +5 -1
- data/lib/kward/cli.rb +14 -2
- data/lib/kward/cli_transcript_formatter.rb +36 -5
- data/lib/kward/compactor.rb +2 -2
- data/lib/kward/config_files.rb +45 -10
- data/lib/kward/conversation.rb +41 -9
- data/lib/kward/memory/manager.rb +131 -14
- data/lib/kward/message_access.rb +6 -0
- data/lib/kward/model/context_usage.rb +11 -10
- data/lib/kward/model/model_info.rb +18 -1
- data/lib/kward/model/payloads.rb +89 -10
- data/lib/kward/model/stream_parser.rb +258 -25
- data/lib/kward/prompt_interface/question_prompt.rb +1 -1
- data/lib/kward/prompt_interface/transcript_renderer.rb +20 -11
- data/lib/kward/prompts.rb +61 -7
- data/lib/kward/rpc/server.rb +7 -2
- data/lib/kward/rpc/session_manager.rb +18 -2
- data/lib/kward/rpc/session_metrics.rb +2 -2
- data/lib/kward/rpc/transcript_normalizer.rb +47 -0
- data/lib/kward/session_store.rb +40 -1
- data/lib/kward/starter_pack_installer.rb +2 -2
- data/lib/kward/tools/fetch_content.rb +41 -0
- data/lib/kward/tools/fetch_raw.rb +40 -0
- data/lib/kward/tools/registry.rb +9 -2
- data/lib/kward/tools/search/web.rb +3 -3
- data/lib/kward/tools/search/web_fetch.rb +202 -0
- data/lib/kward/tools/tool_call.rb +2 -0
- data/lib/kward/version.rb +1 -1
- data/templates/default/fulldoc/html/css/kward.css +1501 -0
- data/templates/default/fulldoc/html/images/kward_logo.png +0 -0
- data/templates/default/fulldoc/html/js/kward.js +296 -0
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/layout/html/breadcrumb.erb +11 -0
- data/templates/default/layout/html/layout.erb +141 -0
- data/templates/default/layout/html/setup.rb +139 -0
- metadata +14 -1
data/lib/kward/cli/sessions.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Kward
|
|
|
30
30
|
|
|
31
31
|
def render_resumed_last_session_transcript(conversation)
|
|
32
32
|
restore_prompt_transcript do
|
|
33
|
-
|
|
33
|
+
runtime_output("Resumed session: #{@active_session.path}")
|
|
34
34
|
render_conversation_transcript(conversation)
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -113,27 +113,27 @@ module Kward
|
|
|
113
113
|
cleanup_replaced_session(previous_session)
|
|
114
114
|
update_assistant_prompt(conversation)
|
|
115
115
|
restore_prompt_transcript do
|
|
116
|
-
|
|
116
|
+
runtime_output("Resumed session: #{@active_session.path}")
|
|
117
117
|
render_conversation_transcript(conversation)
|
|
118
118
|
end
|
|
119
119
|
agent = build_interactive_agent(conversation)
|
|
120
120
|
@prompt.redraw if @prompt.respond_to?(:redraw) && !@prompt.respond_to?(:restore_transcript)
|
|
121
121
|
agent
|
|
122
122
|
rescue StandardError => e
|
|
123
|
-
|
|
123
|
+
runtime_output("Error: #{e.message}")
|
|
124
124
|
nil
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
def navigate_session_tree(session_store)
|
|
128
128
|
return say_sessions_unavailable unless session_store
|
|
129
129
|
unless @active_session
|
|
130
|
-
|
|
130
|
+
runtime_output("No active persisted session.")
|
|
131
131
|
return nil
|
|
132
132
|
end
|
|
133
133
|
|
|
134
134
|
tree_items = session_tree_items(session_store)
|
|
135
135
|
if tree_items.empty?
|
|
136
|
-
|
|
136
|
+
runtime_output("No session tree entries found.")
|
|
137
137
|
return nil
|
|
138
138
|
end
|
|
139
139
|
|
|
@@ -148,19 +148,19 @@ module Kward
|
|
|
148
148
|
return nil unless entry
|
|
149
149
|
|
|
150
150
|
selected_text = apply_session_tree_entry(entry)
|
|
151
|
-
|
|
151
|
+
runtime_output("Moved session tree position to #{entry["id"]}.")
|
|
152
152
|
if selected_text && !selected_text.empty?
|
|
153
153
|
if @prompt.respond_to?(:prefill_input)
|
|
154
154
|
@prompt.prefill_input(selected_text)
|
|
155
155
|
else
|
|
156
|
-
|
|
156
|
+
runtime_output("Selected text for editing:\n#{selected_text}")
|
|
157
157
|
end
|
|
158
158
|
end
|
|
159
159
|
agent = reload_active_session(session_store)
|
|
160
160
|
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
161
161
|
agent
|
|
162
162
|
rescue StandardError => e
|
|
163
|
-
|
|
163
|
+
runtime_output("Session tree error: #{e.message}")
|
|
164
164
|
nil
|
|
165
165
|
end
|
|
166
166
|
|
|
@@ -170,7 +170,7 @@ module Kward
|
|
|
170
170
|
end
|
|
171
171
|
|
|
172
172
|
numbered_labels = labels.each_with_index.map { |label, index| "#{index + 1}. #{label}" }
|
|
173
|
-
|
|
173
|
+
runtime_output((["Session tree:"] + numbered_labels).join("\n"))
|
|
174
174
|
answer = @prompt.ask("Tree entry number>").to_s.strip
|
|
175
175
|
answer.match?(/\A\d+\z/) ? labels[answer.to_i - 1] : nil
|
|
176
176
|
end
|
|
@@ -212,13 +212,13 @@ module Kward
|
|
|
212
212
|
|
|
213
213
|
def rename_session(argument)
|
|
214
214
|
unless @active_session
|
|
215
|
-
|
|
215
|
+
runtime_output("No active persisted session.")
|
|
216
216
|
return
|
|
217
217
|
end
|
|
218
218
|
|
|
219
219
|
@active_session.rename(argument)
|
|
220
220
|
label = @active_session.name ? "Named session: #{@active_session.name}" : "Cleared session name."
|
|
221
|
-
|
|
221
|
+
runtime_output(label)
|
|
222
222
|
end
|
|
223
223
|
|
|
224
224
|
def clone_session(session_store, agent)
|
|
@@ -228,7 +228,7 @@ module Kward
|
|
|
228
228
|
@active_session = track_session(session_store.create_from_conversation(agent.conversation, parent_session: previous_session))
|
|
229
229
|
reset_session_diff(@active_session.path)
|
|
230
230
|
cleanup_replaced_session(previous_session)
|
|
231
|
-
|
|
231
|
+
runtime_output("Cloned session: #{@active_session.path}")
|
|
232
232
|
render_conversation_transcript(agent.conversation)
|
|
233
233
|
agent
|
|
234
234
|
end
|
|
@@ -236,21 +236,21 @@ module Kward
|
|
|
236
236
|
def copy_session_text(conversation, argument)
|
|
237
237
|
target = copy_target(argument)
|
|
238
238
|
unless target
|
|
239
|
-
|
|
239
|
+
runtime_output("Usage: /copy [last|transcript]")
|
|
240
240
|
return
|
|
241
241
|
end
|
|
242
242
|
|
|
243
243
|
content = copy_target_content(conversation, target)
|
|
244
244
|
if content.to_s.empty?
|
|
245
|
-
|
|
245
|
+
runtime_output("Nothing to copy.")
|
|
246
246
|
return
|
|
247
247
|
end
|
|
248
248
|
|
|
249
249
|
result = Clipboard.new(output: $stdout).copy(content)
|
|
250
250
|
if result.success?
|
|
251
|
-
|
|
251
|
+
runtime_output("Copied #{copy_target_label(target)}.")
|
|
252
252
|
else
|
|
253
|
-
|
|
253
|
+
runtime_output("Copy failed: #{result.message}.")
|
|
254
254
|
end
|
|
255
255
|
end
|
|
256
256
|
|
|
@@ -291,13 +291,13 @@ module Kward
|
|
|
291
291
|
def export_session(conversation, argument)
|
|
292
292
|
path = export_path(argument)
|
|
293
293
|
File.write(path, markdown_transcript(conversation))
|
|
294
|
-
|
|
294
|
+
runtime_output("Exported session: #{path}")
|
|
295
295
|
rescue StandardError => e
|
|
296
|
-
|
|
296
|
+
runtime_output("Error: #{e.message}")
|
|
297
297
|
end
|
|
298
298
|
|
|
299
299
|
def say_sessions_unavailable
|
|
300
|
-
|
|
300
|
+
runtime_output("Sessions are unavailable for this interactive loop.")
|
|
301
301
|
nil
|
|
302
302
|
end
|
|
303
303
|
|
|
@@ -316,7 +316,7 @@ module Kward
|
|
|
316
316
|
def select_session_path(session_store)
|
|
317
317
|
sessions = session_store.recent(limit: nil)
|
|
318
318
|
if sessions.empty?
|
|
319
|
-
|
|
319
|
+
runtime_output("No saved sessions found.")
|
|
320
320
|
return nil
|
|
321
321
|
end
|
|
322
322
|
|
|
@@ -330,7 +330,7 @@ module Kward
|
|
|
330
330
|
end
|
|
331
331
|
|
|
332
332
|
numbered_labels = labels.each_with_index.map { |label, index| "#{index + 1}. #{label}" }
|
|
333
|
-
|
|
333
|
+
runtime_output((["Recent sessions:"] + numbered_labels).join("\n"))
|
|
334
334
|
answer = @prompt.ask("Session number or path>").to_s.strip
|
|
335
335
|
if answer.match?(/\A\d+\z/)
|
|
336
336
|
sessions[answer.to_i - 1]&.path
|
data/lib/kward/cli/settings.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Kward
|
|
|
8
8
|
|
|
9
9
|
def configure_settings(conversation = nil)
|
|
10
10
|
unless settings_overlay_available?
|
|
11
|
-
|
|
11
|
+
runtime_output("Settings overlay is unavailable in this prompt.")
|
|
12
12
|
return
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -22,7 +22,7 @@ module Kward
|
|
|
22
22
|
handle_settings_category(category, conversation)
|
|
23
23
|
end
|
|
24
24
|
rescue StandardError => e
|
|
25
|
-
|
|
25
|
+
runtime_output("Settings error: #{e.message}")
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Returns the category labels shown by the interactive settings overlay.
|
|
@@ -168,12 +168,12 @@ module Kward
|
|
|
168
168
|
when /\Aenable memory/, /\Adisable memory/
|
|
169
169
|
set_memory_enabled(!memory_enabled?)
|
|
170
170
|
conversation&.refresh_system_message!
|
|
171
|
-
|
|
171
|
+
runtime_output("Memory #{memory_enabled? ? "enabled" : "disabled"}.")
|
|
172
172
|
when /\Aenable auto-summary/, /\Adisable auto-summary/
|
|
173
173
|
set_memory_auto_summary_enabled(!memory_auto_summary_enabled?)
|
|
174
|
-
|
|
174
|
+
runtime_output("Memory auto-summary #{memory_auto_summary_enabled? ? "enabled" : "disabled"}.")
|
|
175
175
|
when /\Amanage/
|
|
176
|
-
|
|
176
|
+
runtime_output("Use /memory enable|disable|auto-summary enable|disable|core <text>|add <text>|list|forget <id>|promote <id>|relax <id>|inspect|why|summarize.")
|
|
177
177
|
end
|
|
178
178
|
end
|
|
179
179
|
|
|
@@ -221,13 +221,13 @@ module Kward
|
|
|
221
221
|
@prompt.update_overlay_settings(ConfigFiles.update_overlay_settings("width" => width))
|
|
222
222
|
when /\Ashow busy help/, /\Ahide busy help/
|
|
223
223
|
set_composer_busy_help(!composer_busy_help?)
|
|
224
|
-
|
|
224
|
+
runtime_output("Busy help #{composer_busy_help? ? "enabled" : "disabled"}. Restart the TUI to apply this setting.")
|
|
225
225
|
when /\Ashow startup banner/, /\Ahide startup banner/
|
|
226
226
|
set_banner_enabled(!banner_enabled?)
|
|
227
|
-
|
|
227
|
+
runtime_output("Startup banner #{banner_enabled? ? "enabled" : "disabled"}. Restart the TUI to apply this setting.")
|
|
228
228
|
when /\Aenable session auto-resume/, /\Adisable session auto-resume/
|
|
229
229
|
set_session_auto_resume_enabled(!session_auto_resume_enabled?)
|
|
230
|
-
|
|
230
|
+
runtime_output("Session auto-resume #{session_auto_resume_enabled? ? "enabled" : "disabled"}.")
|
|
231
231
|
end
|
|
232
232
|
end
|
|
233
233
|
|
|
@@ -272,15 +272,15 @@ module Kward
|
|
|
272
272
|
case selected.to_s.downcase
|
|
273
273
|
when /\Aenable web search/, /\Adisable web search/
|
|
274
274
|
set_web_search_enabled(!web_search_enabled?)
|
|
275
|
-
|
|
275
|
+
runtime_output("Web search #{web_search_enabled? ? "enabled" : "disabled"}.")
|
|
276
276
|
when /\Aweb search provider/
|
|
277
277
|
configure_web_search_provider
|
|
278
278
|
when /\Aallow model-provider/, /\Adisallow model-provider/
|
|
279
279
|
set_web_search_allow_model_providers(!web_search_allow_model_providers?)
|
|
280
|
-
|
|
280
|
+
runtime_output("Model-provider web search #{web_search_allow_model_providers? ? "enabled" : "disabled"}.")
|
|
281
281
|
when /\Aenable workspace guardrails/, /\Adisable workspace guardrails/
|
|
282
282
|
set_workspace_guardrails_enabled(!workspace_guardrails_enabled?)
|
|
283
|
-
|
|
283
|
+
runtime_output("Workspace guardrails #{workspace_guardrails_enabled? ? "enabled" : "disabled"}.")
|
|
284
284
|
end
|
|
285
285
|
end
|
|
286
286
|
|
|
@@ -336,9 +336,9 @@ module Kward
|
|
|
336
336
|
case selected.to_s.downcase
|
|
337
337
|
when /\Aenable auto-compaction/, /\Adisable auto-compaction/
|
|
338
338
|
set_compaction_enabled(!compaction_enabled?)
|
|
339
|
-
|
|
339
|
+
runtime_output("Auto-compaction #{compaction_enabled? ? "enabled" : "disabled"}.")
|
|
340
340
|
else
|
|
341
|
-
|
|
341
|
+
runtime_output(auto_compaction_status_line) if selected.to_s.downcase.start_with?("status")
|
|
342
342
|
end
|
|
343
343
|
end
|
|
344
344
|
|
|
@@ -388,7 +388,7 @@ module Kward
|
|
|
388
388
|
entries = ConfigFiles.crew_character_labels(personas)
|
|
389
389
|
choices = entries.map { |key, label| key == personas["default"] ? "#{label} (#{key}, current)" : "#{label} (#{key})" }
|
|
390
390
|
if choices.empty?
|
|
391
|
-
|
|
391
|
+
runtime_output("No configured personas found. Edit #{ConfigFiles.config_path} to add personas.")
|
|
392
392
|
return
|
|
393
393
|
end
|
|
394
394
|
|
|
@@ -406,10 +406,10 @@ module Kward
|
|
|
406
406
|
def show_active_instructions_summary(conversation)
|
|
407
407
|
label = ConfigFiles.active_persona_label(workspace_root: current_workspace_root, model: current_model_id, config: safely_read_config.to_h)
|
|
408
408
|
lines = ["Active persona: #{label || "none"}"]
|
|
409
|
-
lines << "Global
|
|
409
|
+
lines << "Global PRINCIPLES.md: #{ConfigFiles.agents_prompt ? "present" : "absent"}"
|
|
410
410
|
lines << "Workspace AGENTS.md: #{ConfigFiles.workspace_agents_prompt(current_workspace_root) ? "present" : "absent"}"
|
|
411
411
|
lines << "Messages: #{conversation.messages.length}" if conversation&.respond_to?(:messages)
|
|
412
|
-
|
|
412
|
+
runtime_output(lines.join("\n"))
|
|
413
413
|
end
|
|
414
414
|
|
|
415
415
|
def configure_logging_settings
|
|
@@ -418,7 +418,7 @@ module Kward
|
|
|
418
418
|
return unless key
|
|
419
419
|
|
|
420
420
|
set_logging_value(key, !logging_enabled?(key))
|
|
421
|
-
|
|
421
|
+
runtime_output("Logging #{key.tr("_", " ")} #{logging_enabled?(key) ? "enabled" : "disabled"}.")
|
|
422
422
|
end
|
|
423
423
|
|
|
424
424
|
def logging_setting_choices
|
|
@@ -463,7 +463,7 @@ module Kward
|
|
|
463
463
|
"Skills: #{ConfigFiles.skills.length}",
|
|
464
464
|
"Prompt templates: #{ConfigFiles.prompt_templates(reserved_commands: BUILTIN_SLASH_COMMAND_NAMES).length}"
|
|
465
465
|
]
|
|
466
|
-
|
|
466
|
+
runtime_output(lines.join("\n"))
|
|
467
467
|
end
|
|
468
468
|
|
|
469
469
|
def update_nested_config(section, values)
|
|
@@ -480,7 +480,7 @@ module Kward
|
|
|
480
480
|
|
|
481
481
|
def login_interactively
|
|
482
482
|
unless login_picker_available?
|
|
483
|
-
|
|
483
|
+
runtime_output("Login provider picker is unavailable in this prompt.")
|
|
484
484
|
return
|
|
485
485
|
end
|
|
486
486
|
|
|
@@ -488,20 +488,22 @@ module Kward
|
|
|
488
488
|
provider = selected_login_provider(selected)
|
|
489
489
|
return unless provider
|
|
490
490
|
|
|
491
|
-
|
|
492
|
-
|
|
491
|
+
run_busy_local_command_and_requeue(activity: "running") do
|
|
492
|
+
login(provider: provider)
|
|
493
|
+
reload_client_config
|
|
494
|
+
end
|
|
493
495
|
rescue StandardError => e
|
|
494
|
-
|
|
496
|
+
runtime_output("Login error: #{e.message}")
|
|
495
497
|
end
|
|
496
498
|
|
|
497
499
|
def configure_model(conversation = nil, models: nil)
|
|
498
500
|
unless model_overlay_available?
|
|
499
|
-
|
|
501
|
+
runtime_output("Model overlay is unavailable in this prompt.")
|
|
500
502
|
return
|
|
501
503
|
end
|
|
502
504
|
|
|
503
|
-
models ||= normalized_available_models
|
|
504
|
-
choices = model_choices(models)
|
|
505
|
+
models ||= normalized_available_models(conversation)
|
|
506
|
+
choices = model_choices(models, conversation)
|
|
505
507
|
selected = @prompt.select("Default model", choices, title: "Models", custom: true)
|
|
506
508
|
return unless selected
|
|
507
509
|
|
|
@@ -513,40 +515,42 @@ module Kward
|
|
|
513
515
|
refresh_conversation_runtime(conversation)
|
|
514
516
|
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
515
517
|
rescue StandardError => e
|
|
516
|
-
|
|
518
|
+
runtime_output("Model error: #{e.message}")
|
|
517
519
|
end
|
|
518
520
|
|
|
519
521
|
# Writes the openrouter catalog output for the terminal CLI flow.
|
|
520
522
|
def print_openrouter_catalog
|
|
521
523
|
unless @client.respond_to?(:openrouter_catalog)
|
|
522
|
-
|
|
524
|
+
runtime_output("OpenRouter catalog is unavailable for this client.")
|
|
523
525
|
return
|
|
524
526
|
end
|
|
525
527
|
|
|
526
528
|
models = Array(@client.openrouter_catalog)
|
|
527
529
|
if models.empty?
|
|
528
|
-
|
|
530
|
+
runtime_output("No OpenRouter catalog models available.")
|
|
529
531
|
else
|
|
530
532
|
ids = models.map { |model| model[:id] || model["id"] || model }.map(&:to_s).reject(&:empty?)
|
|
531
|
-
|
|
533
|
+
runtime_output((["OpenRouter catalog:"] + ids).join("\n"))
|
|
532
534
|
end
|
|
533
535
|
rescue StandardError => e
|
|
534
|
-
|
|
536
|
+
runtime_output("OpenRouter catalog error: #{e.message}")
|
|
535
537
|
end
|
|
536
538
|
|
|
537
539
|
def configure_reasoning(conversation = nil)
|
|
538
540
|
unless model_overlay_available?
|
|
539
|
-
|
|
541
|
+
runtime_output("Reasoning overlay is unavailable in this prompt.")
|
|
540
542
|
return
|
|
541
543
|
end
|
|
542
544
|
|
|
543
|
-
|
|
545
|
+
provider = conversation&.provider || current_model_provider
|
|
546
|
+
model = conversation&.model || current_model_id
|
|
547
|
+
choices = ModelInfo.reasoning_effort_choices(provider, model)
|
|
544
548
|
if choices.empty?
|
|
545
|
-
|
|
549
|
+
runtime_output("Reasoning effort is unavailable for #{provider} #{model}.")
|
|
546
550
|
return
|
|
547
551
|
end
|
|
548
552
|
|
|
549
|
-
selected = @prompt.select("Reasoning effort", reasoning_choices(choices), title: "Reasoning")
|
|
553
|
+
selected = @prompt.select("Reasoning effort", reasoning_choices(choices, conversation), title: "Reasoning")
|
|
550
554
|
return unless selected
|
|
551
555
|
|
|
552
556
|
effort, = choices.find { |_value, label| selected.to_s.downcase.start_with?(label.downcase) }
|
|
@@ -557,7 +561,7 @@ module Kward
|
|
|
557
561
|
refresh_conversation_runtime(conversation)
|
|
558
562
|
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
559
563
|
rescue StandardError => e
|
|
560
|
-
|
|
564
|
+
runtime_output("Reasoning error: #{e.message}")
|
|
561
565
|
end
|
|
562
566
|
|
|
563
567
|
def login_picker_available?
|
|
@@ -596,10 +600,10 @@ module Kward
|
|
|
596
600
|
values.find { |value| choice.to_s.downcase.start_with?(value) }
|
|
597
601
|
end
|
|
598
602
|
|
|
599
|
-
def normalized_available_models
|
|
600
|
-
current_provider = @client.respond_to?(:current_provider) ? @client.current_provider : "Codex"
|
|
601
|
-
current_model = @client.respond_to?(:current_model) ? @client.current_model : nil
|
|
602
|
-
current_reasoning = @client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : nil
|
|
603
|
+
def normalized_available_models(conversation = current_footer_conversation)
|
|
604
|
+
current_provider = conversation.provider || (@client.respond_to?(:current_provider) ? @client.current_provider : "Codex")
|
|
605
|
+
current_model = conversation.model || (@client.respond_to?(:current_model) ? @client.current_model : nil)
|
|
606
|
+
current_reasoning = conversation.reasoning_effort || (@client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : nil)
|
|
603
607
|
models = @client.respond_to?(:available_models) ? Array(@client.available_models) : []
|
|
604
608
|
models.map do |model|
|
|
605
609
|
ModelInfo.normalize(
|
|
@@ -611,13 +615,15 @@ module Kward
|
|
|
611
615
|
end
|
|
612
616
|
end
|
|
613
617
|
|
|
614
|
-
def model_choices(models)
|
|
618
|
+
def model_choices(models, conversation = current_footer_conversation)
|
|
619
|
+
current_provider = conversation.provider || current_model_provider
|
|
620
|
+
current_model = conversation.model || current_model_id
|
|
615
621
|
choices = models.map do |model|
|
|
616
622
|
label = "#{model[:provider]} #{model[:id]}"
|
|
617
623
|
label += " (current)" if model[:current]
|
|
618
624
|
label
|
|
619
625
|
end
|
|
620
|
-
choices.empty? ? ["#{
|
|
626
|
+
choices.empty? ? ["#{current_provider} #{current_model} (current)"] : choices.uniq
|
|
621
627
|
end
|
|
622
628
|
|
|
623
629
|
def selected_model(selected, models)
|
|
@@ -633,8 +639,8 @@ module Kward
|
|
|
633
639
|
end
|
|
634
640
|
end
|
|
635
641
|
|
|
636
|
-
def reasoning_choices(choices)
|
|
637
|
-
current = @client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort
|
|
642
|
+
def reasoning_choices(choices, conversation = current_footer_conversation)
|
|
643
|
+
current = (conversation.reasoning_effort || (@client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : ModelInfo::DEFAULT_REASONING_EFFORT)).to_s
|
|
638
644
|
choices.map do |effort, label|
|
|
639
645
|
text = label.dup
|
|
640
646
|
text += " (current)" if current == effort
|
|
@@ -66,9 +66,11 @@ module Kward
|
|
|
66
66
|
run_busy_local_command_and_requeue(activity: "compacting") { compact_context(agent, argument) }
|
|
67
67
|
[true, nil]
|
|
68
68
|
else
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
if plugin_command_for(name)
|
|
70
|
+
run_busy_local_command_and_requeue(activity: "running") { run_plugin_command(name, argument, agent) }
|
|
71
|
+
else
|
|
72
|
+
[false, nil]
|
|
73
|
+
end
|
|
72
74
|
end
|
|
73
75
|
end
|
|
74
76
|
|
|
@@ -86,7 +88,7 @@ module Kward
|
|
|
86
88
|
lines << "File: #{@active_session.path}"
|
|
87
89
|
end
|
|
88
90
|
lines.compact!
|
|
89
|
-
|
|
91
|
+
runtime_output(lines.join("\n"))
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
def auto_compaction_status_line
|
data/lib/kward/cli/stats.rb
CHANGED
|
@@ -53,10 +53,10 @@ module Kward
|
|
|
53
53
|
# Writes the stats output for the terminal CLI flow.
|
|
54
54
|
def print_stats(argument)
|
|
55
55
|
result = TelemetryStats.new.collect(argument)
|
|
56
|
-
|
|
56
|
+
runtime_output(TelemetryStats.format(result))
|
|
57
57
|
rescue ArgumentError => e
|
|
58
58
|
message = e.message == TelemetryStats::USAGE ? e.message : "#{e.message}\n#{TelemetryStats::USAGE}"
|
|
59
|
-
|
|
59
|
+
runtime_output(message)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Command-line frontend that coordinates terminal interaction, sessions, tools, and model turns.
|
|
4
|
+
class CLI
|
|
5
|
+
# System prompt inspection command helpers.
|
|
6
|
+
module Sysprompt
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def print_sysprompt(arguments)
|
|
10
|
+
raw = parse_sysprompt_arguments(arguments)
|
|
11
|
+
conversation = new_conversation
|
|
12
|
+
content = conversation.system_message.fetch(:content)
|
|
13
|
+
if raw
|
|
14
|
+
@prompt.say(content)
|
|
15
|
+
else
|
|
16
|
+
@prompt.say(render_markdown_transcript(render_sysprompt_sections(conversation)))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parse_sysprompt_arguments(arguments)
|
|
21
|
+
raw = false
|
|
22
|
+
arguments.each do |argument|
|
|
23
|
+
case argument
|
|
24
|
+
when "--raw"
|
|
25
|
+
raw = true
|
|
26
|
+
else
|
|
27
|
+
raise ArgumentError, command_usage("sysprompt")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
raw
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render_sysprompt_sections(conversation)
|
|
34
|
+
sections = Prompts.prompt_sections(
|
|
35
|
+
workspace_root: conversation.workspace_root,
|
|
36
|
+
model: conversation.model,
|
|
37
|
+
reasoning_effort: conversation.reasoning_effort,
|
|
38
|
+
memory_context: conversation.memory_context,
|
|
39
|
+
plugin_context: conversation.last_plugin_prompt_context
|
|
40
|
+
)
|
|
41
|
+
lines = ["# Kward System Prompt", "", "Workspace: #{conversation.workspace_root}"]
|
|
42
|
+
lines << "Model: #{[conversation.provider, conversation.model].compact.join(" / ")}" unless conversation.model.to_s.empty?
|
|
43
|
+
lines << "Reasoning effort: #{conversation.reasoning_effort}" unless conversation.reasoning_effort.to_s.empty?
|
|
44
|
+
lines << "Memory: not included; memory context is retrieved per user turn."
|
|
45
|
+
sections.each do |section|
|
|
46
|
+
lines << ""
|
|
47
|
+
lines << "## #{section.fetch(:label)}"
|
|
48
|
+
source = section[:source]
|
|
49
|
+
lines << "Source: #{source}" unless source.to_s.empty?
|
|
50
|
+
lines << ""
|
|
51
|
+
lines << section.fetch(:content)
|
|
52
|
+
end
|
|
53
|
+
lines.join("\n")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -10,7 +10,7 @@ module Kward
|
|
|
10
10
|
name = tool_call_name(tool_call)
|
|
11
11
|
args = tool_call_args(tool_call)
|
|
12
12
|
text = content.to_s
|
|
13
|
-
return error_tool_summary(name, args, text) if
|
|
13
|
+
return error_tool_summary(name, args, text) if tool_result_failed?(text)
|
|
14
14
|
|
|
15
15
|
case name
|
|
16
16
|
when "read_file"
|
|
@@ -26,6 +26,10 @@ module Kward
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
def tool_result_failed?(content)
|
|
30
|
+
content.to_s.start_with?("Error:", "Declined:", "Cancelled.")
|
|
31
|
+
end
|
|
32
|
+
|
|
29
33
|
def limit_tool_output_lines(content, line_limit)
|
|
30
34
|
lines = content.to_s.lines
|
|
31
35
|
return content.to_s if lines.length <= line_limit
|
data/lib/kward/cli.rb
CHANGED
|
@@ -39,6 +39,7 @@ require_relative "workspace"
|
|
|
39
39
|
require_relative "cli/commands"
|
|
40
40
|
require_relative "cli/auth_commands"
|
|
41
41
|
require_relative "cli/doctor"
|
|
42
|
+
require_relative "cli/sysprompt"
|
|
42
43
|
require_relative "cli/stats"
|
|
43
44
|
require_relative "cli/runtime_helpers"
|
|
44
45
|
require_relative "cli/slash_commands"
|
|
@@ -69,6 +70,7 @@ module Kward
|
|
|
69
70
|
include CLI::Commands
|
|
70
71
|
include CLI::AuthCommands
|
|
71
72
|
include CLI::Doctor
|
|
73
|
+
include CLI::Sysprompt
|
|
72
74
|
include CLI::Stats
|
|
73
75
|
include CLI::RuntimeHelpers
|
|
74
76
|
include CLI::SlashCommands
|
|
@@ -159,6 +161,16 @@ module Kward
|
|
|
159
161
|
return
|
|
160
162
|
end
|
|
161
163
|
|
|
164
|
+
if @argv.first == "sysprompt"
|
|
165
|
+
if help_option_arguments?(@argv[1..] || [])
|
|
166
|
+
print_command_help("sysprompt")
|
|
167
|
+
return
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
print_sysprompt(@argv[1..] || [])
|
|
171
|
+
return
|
|
172
|
+
end
|
|
173
|
+
|
|
162
174
|
if @argv.first == "rpc"
|
|
163
175
|
if help_option_arguments?(@argv[1..] || [])
|
|
164
176
|
print_command_help("rpc")
|
|
@@ -303,13 +315,13 @@ module Kward
|
|
|
303
315
|
pending_inputs = run_interactive_turn(agent, input, display_input: display_input)
|
|
304
316
|
pending_inputs.reverse_each { |pending_input| @pending_inputs.unshift(pending_input) }
|
|
305
317
|
rescue StandardError => e
|
|
306
|
-
|
|
318
|
+
runtime_output("Error: #{e.message}")
|
|
307
319
|
end
|
|
308
320
|
end
|
|
309
321
|
|
|
310
322
|
agent.conversation
|
|
311
323
|
rescue Interrupt
|
|
312
|
-
|
|
324
|
+
runtime_output("Goodbye.")
|
|
313
325
|
agent&.conversation
|
|
314
326
|
ensure
|
|
315
327
|
begin
|
|
@@ -14,13 +14,20 @@ module Kward
|
|
|
14
14
|
return direct.to_s unless direct.to_s.empty?
|
|
15
15
|
|
|
16
16
|
content = MessageAccess.content(message)
|
|
17
|
-
|
|
17
|
+
if content.is_a?(Array)
|
|
18
|
+
text = content.filter_map do |part|
|
|
19
|
+
type = MessageAccess.value(part, :type)
|
|
20
|
+
next unless ["thinking", "reasoning"].include?(type)
|
|
21
|
+
|
|
22
|
+
MessageAccess.value(part, :thinking) || MessageAccess.value(part, :reasoning) || MessageAccess.value(part, :text)
|
|
23
|
+
end.join("\n")
|
|
24
|
+
return text unless text.empty?
|
|
25
|
+
end
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
next unless ["thinking", "reasoning"].include?(type)
|
|
27
|
+
response_items(message).filter_map do |item|
|
|
28
|
+
next unless MessageAccess.value(item, :type) == "reasoning"
|
|
22
29
|
|
|
23
|
-
MessageAccess.value(
|
|
30
|
+
response_item_text(MessageAccess.value(item, :summary)).empty? ? response_item_text(MessageAccess.value(item, :content)) : response_item_text(MessageAccess.value(item, :summary))
|
|
24
31
|
end.join("\n")
|
|
25
32
|
end
|
|
26
33
|
|
|
@@ -33,6 +40,18 @@ module Kward
|
|
|
33
40
|
end
|
|
34
41
|
end
|
|
35
42
|
|
|
43
|
+
def assistant_content_text(message)
|
|
44
|
+
text = content_text(MessageAccess.content(message))
|
|
45
|
+
return text unless text.empty?
|
|
46
|
+
|
|
47
|
+
response_items(message).filter_map do |item|
|
|
48
|
+
next unless MessageAccess.value(item, :type) == "message"
|
|
49
|
+
next if MessageAccess.value(item, :phase).to_s == "commentary"
|
|
50
|
+
|
|
51
|
+
response_item_text(MessageAccess.value(item, :content))
|
|
52
|
+
end.join
|
|
53
|
+
end
|
|
54
|
+
|
|
36
55
|
def display_text(message)
|
|
37
56
|
display_content = MessageAccess.display_content(message)
|
|
38
57
|
return display_content.to_s unless display_content.nil?
|
|
@@ -96,6 +115,18 @@ module Kward
|
|
|
96
115
|
end
|
|
97
116
|
end
|
|
98
117
|
|
|
118
|
+
def response_items(message)
|
|
119
|
+
MessageAccess.response_items(message)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def response_item_text(parts)
|
|
123
|
+
Array(parts).filter_map do |part|
|
|
124
|
+
next unless part.is_a?(Hash)
|
|
125
|
+
|
|
126
|
+
MessageAccess.value(part, :text) || MessageAccess.value(part, :refusal)
|
|
127
|
+
end.join
|
|
128
|
+
end
|
|
129
|
+
|
|
99
130
|
def image_part_reference(part)
|
|
100
131
|
data = MessageAccess.value(part, :data)
|
|
101
132
|
path = MessageAccess.value(part, :path)
|