kward 0.71.0 → 0.73.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 +30 -0
- data/CHANGELOG.md +93 -0
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- data/doc/editor.md +415 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +123 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +75 -5
- data/doc/session-management.md +35 -1
- data/doc/shell.md +332 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +79 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/ansi.rb +62 -23
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/rendering.rb +4 -1
- data/lib/kward/cli/runtime_helpers.rb +268 -4
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +217 -9
- data/lib/kward/cli/slash_commands.rb +628 -2
- data/lib/kward/cli/tabs.rb +725 -0
- data/lib/kward/cli/tool_summaries.rb +6 -0
- data/lib/kward/cli.rb +150 -26
- data/lib/kward/clipboard.rb +2 -3
- data/lib/kward/compactor.rb +7 -19
- data/lib/kward/config_files.rb +145 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +559 -0
- 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/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +84 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +510 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1218 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +354 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1812 -0
- data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +244 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1271 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +422 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +288 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +451 -57
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/question_prompt.rb +99 -56
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +19 -3
- data/lib/kward/prompt_interface/selection_prompt.rb +10 -19
- data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +366 -222
- data/lib/kward/prompts/commands.rb +9 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/memory_methods.rb +83 -0
- data/lib/kward/rpc/server.rb +169 -83
- data/lib/kward/rpc/session_manager.rb +45 -121
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- 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/session_store.rb +44 -0
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/terminal_keys.rb +84 -0
- data/lib/kward/terminal_sequences.rb +42 -0
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +204 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +62 -16
- data/lib/kward/tools/tool_call.rb +10 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +93 -0
- data/lib/kward/workers/job.rb +99 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/queue_runner.rb +166 -0
- data/lib/kward/workers/queue_store.rb +112 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +10 -0
- data/lib/kward/workspace.rb +125 -87
- data/templates/default/fulldoc/html/css/kward.css +140 -36
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -0
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +23 -34
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +67 -1
|
@@ -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
|
|
@@ -295,12 +299,21 @@ module Kward
|
|
|
295
299
|
rpc_sessions = @mutex.synchronize { @sessions.values.dup }
|
|
296
300
|
rpc_sessions.reverse_each do |rpc_session|
|
|
297
301
|
next unless session_idle?(rpc_session)
|
|
302
|
+
next unless rpc_session.session.respond_to?(:delete_if_unused)
|
|
303
|
+
next unless rpc_session.session.delete_if_unused
|
|
298
304
|
|
|
299
|
-
|
|
305
|
+
remove_live_session(rpc_session)
|
|
300
306
|
end
|
|
301
307
|
{ closed: true }
|
|
302
308
|
end
|
|
303
309
|
|
|
310
|
+
# Stops all live RPC session workers during server shutdown.
|
|
311
|
+
def shutdown_sessions
|
|
312
|
+
rpc_sessions = @mutex.synchronize { @sessions.values.dup }
|
|
313
|
+
rpc_sessions.reverse_each { |rpc_session| close_rpc_session(rpc_session) if session_idle?(rpc_session) }
|
|
314
|
+
{ closed: true }
|
|
315
|
+
end
|
|
316
|
+
|
|
304
317
|
# Returns the normalized transcript for the active RPC session.
|
|
305
318
|
def transcript(session_id:)
|
|
306
319
|
rpc_session = fetch_session(session_id)
|
|
@@ -321,10 +334,7 @@ module Kward
|
|
|
321
334
|
content = plugin_command ? input.to_s : user_turn_content(expand_prompt_input(input), normalized_attachments)
|
|
322
335
|
streaming_behavior = validate_streaming_behavior(default_streaming_behavior(rpc_session, streaming_behavior), rpc_session: rpc_session)
|
|
323
336
|
if streaming_behavior == "steer"
|
|
324
|
-
|
|
325
|
-
return steered_turn if steered_turn
|
|
326
|
-
|
|
327
|
-
streaming_behavior = "followUp"
|
|
337
|
+
return steer_running_turn(rpc_session, content)
|
|
328
338
|
end
|
|
329
339
|
turn = Turn.new(
|
|
330
340
|
id: SecureRandom.uuid,
|
|
@@ -385,89 +395,11 @@ module Kward
|
|
|
385
395
|
run_plugin_command(session_id: session_id, command: name, arguments: arguments)
|
|
386
396
|
end
|
|
387
397
|
|
|
388
|
-
def memory_manager
|
|
389
|
-
Memory::Manager.for_config_dir(@config_dir)
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
def memory_status
|
|
393
|
-
manager = memory_manager
|
|
394
|
-
{ enabled: manager.enabled?, autoSummary: manager.auto_summary_enabled?, paths: manager.paths }
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
def memory_enable
|
|
398
|
-
memory_manager.enable
|
|
399
|
-
{ enabled: true }
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
def memory_disable
|
|
403
|
-
memory_manager.disable
|
|
404
|
-
{ enabled: false }
|
|
405
|
-
end
|
|
406
|
-
|
|
407
|
-
def memory_auto_summary_enable
|
|
408
|
-
memory_manager.auto_summary_enable
|
|
409
|
-
{ autoSummary: true }
|
|
410
|
-
end
|
|
411
|
-
|
|
412
|
-
def memory_auto_summary_disable
|
|
413
|
-
memory_manager.auto_summary_disable
|
|
414
|
-
{ autoSummary: false }
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
def memory_list(include_inactive: false, workspace_root: Dir.pwd)
|
|
418
|
-
memory_manager.hierarchy(include_inactive: include_inactive, workspace_root: workspace_root)
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
def memory_add(text:, scope: nil, tags: [])
|
|
422
|
-
{ memory: memory_manager.add_soft(text, scope: scope || "global", tags: tags) }
|
|
423
|
-
end
|
|
424
|
-
|
|
425
|
-
def memory_add_core(text:, scope: nil, tags: [])
|
|
426
|
-
{ memory: memory_manager.add_core(text, scope: scope || "global", tags: tags) }
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
def memory_forget(id:)
|
|
430
|
-
{ forgotten: memory_manager.forget_memory(id) }
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
def memory_promote(id:)
|
|
434
|
-
{ memory: memory_manager.promote_memory(id) }
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
def memory_relax(id:, workspace_root: Dir.pwd)
|
|
438
|
-
{ memory: memory_manager.relax_core(id, workspace_root: workspace_root) }
|
|
439
|
-
end
|
|
440
|
-
|
|
441
|
-
def memory_inspect
|
|
442
|
-
memory_manager.inspect_memory
|
|
443
|
-
end
|
|
444
|
-
|
|
445
|
-
def memory_why(session_id: nil)
|
|
446
|
-
if session_id
|
|
447
|
-
rpc_session = fetch_session(session_id)
|
|
448
|
-
return rpc_session.conversation.last_memory_retrieval || memory_manager.explain_retrieval
|
|
449
|
-
end
|
|
450
|
-
memory_manager.explain_retrieval
|
|
451
|
-
end
|
|
452
|
-
|
|
453
|
-
def memory_summarize(session_id:)
|
|
454
|
-
rpc_session = fetch_session(session_id)
|
|
455
|
-
records = memory_manager.summarize_conversation(rpc_session.conversation, client: @client)
|
|
456
|
-
persist_memory_state(rpc_session)
|
|
457
|
-
{ memories: records }
|
|
458
|
-
end
|
|
459
|
-
|
|
460
398
|
def run_plugin_command(session_id:, command:, arguments: "")
|
|
461
399
|
rpc_session = fetch_session(session_id)
|
|
462
400
|
command = plugin_registry.command_for(command.to_s.delete_prefix("/")) || raise(ArgumentError, "Unknown plugin command: #{command}")
|
|
463
401
|
output = []
|
|
464
|
-
context =
|
|
465
|
-
conversation: rpc_session.conversation,
|
|
466
|
-
args: arguments.to_s,
|
|
467
|
-
session: rpc_session.session,
|
|
468
|
-
workspace_root: rpc_session.workspace_root,
|
|
469
|
-
say_callback: lambda { |message| output << message.to_s }
|
|
470
|
-
)
|
|
402
|
+
context = plugin_context(rpc_session, args: arguments.to_s, say_callback: lambda { |message| output << message.to_s })
|
|
471
403
|
result = command.handler.call(arguments.to_s, context)
|
|
472
404
|
output = rpc_session.plugin_output.shift(rpc_session.plugin_output.length) + output
|
|
473
405
|
{ command: command.name, output: output, result: result.nil? ? nil : result.to_s }
|
|
@@ -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
|
|
|
@@ -709,10 +645,6 @@ module Kward
|
|
|
709
645
|
pending_turn_count(rpc_session.id).zero?
|
|
710
646
|
end
|
|
711
647
|
|
|
712
|
-
def active_session_count(workspace_root)
|
|
713
|
-
@mutex.synchronize { @sessions.values.count { |rpc_session| rpc_session.workspace_root == workspace_root } }
|
|
714
|
-
end
|
|
715
|
-
|
|
716
648
|
def tool_calls(message)
|
|
717
649
|
MessageAccess.tool_calls(message)
|
|
718
650
|
end
|
|
@@ -721,10 +653,6 @@ module Kward
|
|
|
721
653
|
MessageAccess.role(message)
|
|
722
654
|
end
|
|
723
655
|
|
|
724
|
-
def message_content(message)
|
|
725
|
-
MessageAccess.content(message)
|
|
726
|
-
end
|
|
727
|
-
|
|
728
656
|
def session_tree_helper(rpc_session)
|
|
729
657
|
SessionTree.new(rpc_session)
|
|
730
658
|
end
|
|
@@ -913,9 +841,7 @@ module Kward
|
|
|
913
841
|
end
|
|
914
842
|
|
|
915
843
|
def close_rpc_session(rpc_session, delete_unused: true)
|
|
916
|
-
|
|
917
|
-
stop_worker(rpc_session)
|
|
918
|
-
stop_footer_worker(rpc_session)
|
|
844
|
+
remove_live_session(rpc_session)
|
|
919
845
|
rpc_session.session.delete_if_unused if delete_unused && rpc_session.session.respond_to?(:delete_if_unused)
|
|
920
846
|
end
|
|
921
847
|
|
|
@@ -928,12 +854,16 @@ module Kward
|
|
|
928
854
|
next unless rpc_session.session.respond_to?(:delete_if_unused)
|
|
929
855
|
next unless rpc_session.session.delete_if_unused
|
|
930
856
|
|
|
931
|
-
|
|
932
|
-
stop_worker(rpc_session)
|
|
933
|
-
stop_footer_worker(rpc_session)
|
|
857
|
+
remove_live_session(rpc_session)
|
|
934
858
|
end
|
|
935
859
|
end
|
|
936
860
|
|
|
861
|
+
def remove_live_session(rpc_session)
|
|
862
|
+
@mutex.synchronize { @sessions.delete(rpc_session.id) }
|
|
863
|
+
stop_worker(rpc_session)
|
|
864
|
+
stop_footer_worker(rpc_session)
|
|
865
|
+
end
|
|
866
|
+
|
|
937
867
|
def stop_worker(rpc_session)
|
|
938
868
|
worker = rpc_session.worker
|
|
939
869
|
return unless worker&.alive?
|
|
@@ -1042,7 +972,7 @@ module Kward
|
|
|
1042
972
|
end
|
|
1043
973
|
|
|
1044
974
|
def default_session_name(input)
|
|
1045
|
-
|
|
975
|
+
SessionNaming.default_name(input)
|
|
1046
976
|
end
|
|
1047
977
|
|
|
1048
978
|
def turn_display_input(turn)
|
|
@@ -1076,21 +1006,23 @@ module Kward
|
|
|
1076
1006
|
|
|
1077
1007
|
turn.steering.submit(input)
|
|
1078
1008
|
turn_payload(turn)
|
|
1079
|
-
rescue StandardError
|
|
1080
|
-
nil
|
|
1081
1009
|
end
|
|
1082
1010
|
|
|
1083
|
-
def
|
|
1084
|
-
|
|
1085
|
-
command = plugin_registry.command_for(turn.plugin_command_name) || raise(ArgumentError, "Unknown plugin command: #{turn.plugin_command_name}")
|
|
1086
|
-
output = []
|
|
1087
|
-
context = PluginRegistry::Context.new(
|
|
1011
|
+
def plugin_context(rpc_session, args: nil, say_callback:)
|
|
1012
|
+
PluginRegistry::Context.new(
|
|
1088
1013
|
conversation: rpc_session.conversation,
|
|
1089
|
-
args:
|
|
1014
|
+
args: args,
|
|
1090
1015
|
session: rpc_session.session,
|
|
1091
1016
|
workspace_root: rpc_session.workspace_root,
|
|
1092
|
-
say_callback:
|
|
1017
|
+
say_callback: say_callback
|
|
1093
1018
|
)
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
def run_plugin_turn(rpc_session, turn)
|
|
1022
|
+
turn.cancellation&.raise_if_cancelled!
|
|
1023
|
+
command = plugin_registry.command_for(turn.plugin_command_name) || raise(ArgumentError, "Unknown plugin command: #{turn.plugin_command_name}")
|
|
1024
|
+
output = []
|
|
1025
|
+
context = plugin_context(rpc_session, args: turn.plugin_arguments.to_s, say_callback: lambda { |message| output << message.to_s })
|
|
1094
1026
|
result = command.handler.call(turn.plugin_arguments.to_s, context)
|
|
1095
1027
|
answer = (output + [result]).compact.map(&:to_s).reject(&:empty?).join("\n")
|
|
1096
1028
|
unless answer.empty?
|
|
@@ -1103,12 +1035,7 @@ module Kward
|
|
|
1103
1035
|
def notify_plugin_transcript_event(rpc_session, event)
|
|
1104
1036
|
return if plugin_registry.transcript_event_handlers.empty?
|
|
1105
1037
|
|
|
1106
|
-
context =
|
|
1107
|
-
conversation: rpc_session.conversation,
|
|
1108
|
-
session: rpc_session.session,
|
|
1109
|
-
workspace_root: rpc_session.workspace_root,
|
|
1110
|
-
say_callback: lambda { |message| rpc_session.plugin_output << message.to_s }
|
|
1111
|
-
)
|
|
1038
|
+
context = plugin_context(rpc_session, say_callback: lambda { |message| rpc_session.plugin_output << message.to_s })
|
|
1112
1039
|
plugin_registry.notify_transcript_event(event, context)
|
|
1113
1040
|
end
|
|
1114
1041
|
|
|
@@ -1124,6 +1051,8 @@ module Kward
|
|
|
1124
1051
|
emit_turn_event(turn, "modelRetry", retry_event_payload(event))
|
|
1125
1052
|
when Events::Steering
|
|
1126
1053
|
emit_turn_event(turn, "turnSteered", { input: event.input, createdAt: event.created_at })
|
|
1054
|
+
when Events::SteeringApplied
|
|
1055
|
+
emit_turn_event(turn, "steeringApplied", { count: event.count })
|
|
1127
1056
|
when Events::ToolCall
|
|
1128
1057
|
emit_turn_event(turn, "toolCall", normalized_tool_event_payload(event.tool_call))
|
|
1129
1058
|
when Events::ToolResult
|
|
@@ -1160,12 +1089,7 @@ module Kward
|
|
|
1160
1089
|
return clear_footer_update(rpc_session) unless renderer
|
|
1161
1090
|
|
|
1162
1091
|
text = begin
|
|
1163
|
-
context =
|
|
1164
|
-
conversation: rpc_session.conversation,
|
|
1165
|
-
session: rpc_session.session,
|
|
1166
|
-
workspace_root: rpc_session.workspace_root,
|
|
1167
|
-
say_callback: lambda { |message| rpc_session.plugin_output << message.to_s }
|
|
1168
|
-
)
|
|
1092
|
+
context = plugin_context(rpc_session, say_callback: lambda { |message| rpc_session.plugin_output << message.to_s })
|
|
1169
1093
|
renderer.call(context).to_s.gsub(/\s+/, " ").strip
|
|
1170
1094
|
rescue StandardError => e
|
|
1171
1095
|
warn "Warning: Kward plugin footer error: #{e.message}"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require_relative "../message_access"
|
|
2
2
|
require_relative "../message_text"
|
|
3
|
+
require_relative "../session_tree_nodes"
|
|
3
4
|
require_relative "../session_tree_tool_display"
|
|
4
|
-
require_relative "../tools/tool_call"
|
|
5
5
|
|
|
6
6
|
# Namespace for the Kward CLI agent runtime.
|
|
7
7
|
module Kward
|
|
@@ -28,13 +28,14 @@ module Kward
|
|
|
28
28
|
#
|
|
29
29
|
# @return [Array<Hash>] rows for the `session/tree` RPC method
|
|
30
30
|
def rows
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
tree_nodes = SessionTreeNodes.new(roots: @roots, current_leaf: @current_leaf)
|
|
32
|
+
active_path = tree_nodes.active_path
|
|
33
|
+
tool_calls_by_id = tree_nodes.tool_calls
|
|
34
|
+
visible_roots = tree_nodes.visible_roots
|
|
34
35
|
multiple_roots = visible_roots.length > 1
|
|
35
36
|
result = []
|
|
36
37
|
|
|
37
|
-
stack = visible_roots.sort_by { |root|
|
|
38
|
+
stack = visible_roots.sort_by { |root| tree_nodes.contains_active_path?(root, active_path) ? 0 : 1 }.each_with_index.map do |root, index|
|
|
38
39
|
[root, multiple_roots ? 1 : 0, multiple_roots, multiple_roots, index == visible_roots.length - 1, [], multiple_roots]
|
|
39
40
|
end.reverse
|
|
40
41
|
|
|
@@ -60,7 +61,7 @@ module Kward
|
|
|
60
61
|
prefix: tree_prefix(display_indent, gutters, show_connector && !virtual_root_child, is_last, !node[:children].empty?)
|
|
61
62
|
}.compact
|
|
62
63
|
|
|
63
|
-
children = node[:children].sort_by { |child|
|
|
64
|
+
children = node[:children].sort_by { |child| tree_nodes.contains_active_path?(child, active_path) ? 0 : 1 }
|
|
64
65
|
multiple_children = children.length > 1
|
|
65
66
|
child_indent = if multiple_children
|
|
66
67
|
indent + 1
|
|
@@ -80,112 +81,6 @@ module Kward
|
|
|
80
81
|
|
|
81
82
|
private
|
|
82
83
|
|
|
83
|
-
def tree_active_path(roots, leaf_id)
|
|
84
|
-
by_id = tree_entries_by_id(roots)
|
|
85
|
-
ids = []
|
|
86
|
-
current = by_id[leaf_id.to_s]
|
|
87
|
-
seen = {}
|
|
88
|
-
while current && !seen[current["id"].to_s]
|
|
89
|
-
seen[current["id"].to_s] = true
|
|
90
|
-
ids << current["id"].to_s
|
|
91
|
-
current = by_id[current["parentId"].to_s]
|
|
92
|
-
end
|
|
93
|
-
ids
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def tree_entries_by_id(roots)
|
|
97
|
-
roots.each_with_object({}) do |root, map|
|
|
98
|
-
stack = [root]
|
|
99
|
-
seen = {}
|
|
100
|
-
until stack.empty?
|
|
101
|
-
node = stack.pop
|
|
102
|
-
next if seen[node.object_id]
|
|
103
|
-
|
|
104
|
-
seen[node.object_id] = true
|
|
105
|
-
entry = node["entry"] || {}
|
|
106
|
-
map[entry["id"].to_s] = entry unless entry["id"].to_s.empty?
|
|
107
|
-
stack.concat(Array(node["children"]))
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def visible_tree_nodes(node)
|
|
113
|
-
results = {}
|
|
114
|
-
stack = [[node, false, {}]]
|
|
115
|
-
|
|
116
|
-
until stack.empty?
|
|
117
|
-
current, visited, seen = stack.pop
|
|
118
|
-
node_key = current.object_id
|
|
119
|
-
next if seen[node_key]
|
|
120
|
-
|
|
121
|
-
if visited
|
|
122
|
-
children = Array(current["children"]).flat_map { |child| results[child.object_id] || [] }
|
|
123
|
-
results[node_key] = if hidden_tree_entry?(current["entry"] || {})
|
|
124
|
-
children
|
|
125
|
-
else
|
|
126
|
-
[{ source: current, children: children }]
|
|
127
|
-
end
|
|
128
|
-
else
|
|
129
|
-
branch_seen = seen.merge(node_key => true)
|
|
130
|
-
stack << [current, true, seen]
|
|
131
|
-
Array(current["children"]).reverse_each { |child| stack << [child, false, branch_seen] unless branch_seen[child.object_id] }
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
results[node.object_id] || []
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def hidden_tree_entry?(entry)
|
|
139
|
-
return false if @current_leaf && entry["id"].to_s == @current_leaf.to_s
|
|
140
|
-
return false unless entry["type"] == "message"
|
|
141
|
-
|
|
142
|
-
message = entry["message"]
|
|
143
|
-
return false unless message.is_a?(Hash) && MessageAccess.role(message) == "assistant"
|
|
144
|
-
|
|
145
|
-
content = MessageAccess.content(message)
|
|
146
|
-
content_tool_calls = content.is_a?(Array) && content.any? { |part| ToolCall.value(part, :type) == "toolCall" }
|
|
147
|
-
(content_tool_calls && !tree_text_content?(content)) || (!MessageAccess.tool_calls(message).empty? && MessageText.full_text(message).empty?)
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def tree_text_content?(content)
|
|
151
|
-
Array(content).any? { |part| ToolCall.value(part, :type) == "text" && ToolCall.value(part, :text).to_s.strip != "" }
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def tree_contains_active_path?(node, active_path)
|
|
155
|
-
stack = [node]
|
|
156
|
-
seen = {}
|
|
157
|
-
until stack.empty?
|
|
158
|
-
current = stack.pop
|
|
159
|
-
next if seen[current.object_id]
|
|
160
|
-
|
|
161
|
-
seen[current.object_id] = true
|
|
162
|
-
entry_id = (current[:source]["entry"] || {})["id"].to_s
|
|
163
|
-
return true if active_path.include?(entry_id)
|
|
164
|
-
|
|
165
|
-
stack.concat(current[:children])
|
|
166
|
-
end
|
|
167
|
-
false
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def tree_tool_calls(roots)
|
|
171
|
-
roots.each_with_object({}) do |root, tool_calls_by_id|
|
|
172
|
-
stack = [root]
|
|
173
|
-
seen = {}
|
|
174
|
-
until stack.empty?
|
|
175
|
-
node = stack.pop
|
|
176
|
-
next if seen[node.object_id]
|
|
177
|
-
|
|
178
|
-
seen[node.object_id] = true
|
|
179
|
-
entry = node["entry"] || {}
|
|
180
|
-
message = entry["message"]
|
|
181
|
-
if entry["type"] == "message" && message.is_a?(Hash) && MessageAccess.role(message) == "assistant"
|
|
182
|
-
MessageAccess.tool_calls(message).each { |tool_call| tool_calls_by_id[ToolCall.id(tool_call).to_s] = tool_call }
|
|
183
|
-
end
|
|
184
|
-
stack.concat(Array(node["children"]))
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
84
|
def tree_entry_display(entry, tool_calls_by_id = {})
|
|
190
85
|
case entry["type"]
|
|
191
86
|
when "message"
|
|
@@ -237,12 +132,11 @@ module Kward
|
|
|
237
132
|
end
|
|
238
133
|
|
|
239
134
|
def display_message_text(message)
|
|
240
|
-
|
|
135
|
+
SessionTreeNodes.truncate_text(MessageText.full_text(message))
|
|
241
136
|
end
|
|
242
137
|
|
|
243
138
|
def truncate_tree_text(text)
|
|
244
|
-
|
|
245
|
-
normalized.length > 120 ? "#{normalized.slice(0, 117)}..." : normalized
|
|
139
|
+
SessionTreeNodes.truncate_text(text)
|
|
246
140
|
end
|
|
247
141
|
end
|
|
248
142
|
end
|
|
@@ -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
|
data/lib/kward/session_diff.rb
CHANGED
|
@@ -25,20 +25,37 @@ module Kward
|
|
|
25
25
|
new
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def self.content_from_session_file(path)
|
|
29
|
+
records = File.readlines(path, chomp: true).filter_map { |line| parse_record(line) }
|
|
30
|
+
content_from_records(records)
|
|
31
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
32
|
+
""
|
|
33
|
+
end
|
|
34
|
+
|
|
28
35
|
def self.from_records(records)
|
|
29
36
|
execution_records = records.select { |record| record["type"] == "tool_execution_end" }
|
|
30
37
|
source_records = execution_records.empty? ? records : execution_records
|
|
31
38
|
source_records.each_with_object(new) do |record, diff|
|
|
32
39
|
if record["type"] == "tool_execution_end"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
diff.add_diff(record.dig("result", "diff"))
|
|
40
|
+
diff.add_diff(record_diff(record))
|
|
36
41
|
elsif record["type"] == "message" && (record.dig("message", "role") == "tool" || record.dig("message", :role) == "tool")
|
|
37
42
|
diff.add_tool_result(record.dig("message", "content") || record.dig("message", :content))
|
|
38
43
|
end
|
|
39
44
|
end
|
|
40
45
|
end
|
|
41
46
|
|
|
47
|
+
def self.content_from_records(records)
|
|
48
|
+
records.filter_map { |record| record_diff(record) }.join("\n")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.record_diff(record)
|
|
52
|
+
return nil unless record["type"] == "tool_execution_end"
|
|
53
|
+
return nil if record["isError"] || record.dig("result", "isError")
|
|
54
|
+
|
|
55
|
+
diff = record.dig("result", "diff").to_s
|
|
56
|
+
diff.empty? ? nil : diff
|
|
57
|
+
end
|
|
58
|
+
|
|
42
59
|
def self.count(diff)
|
|
43
60
|
if (stats = truncated_diff_stats(diff))
|
|
44
61
|
return stats
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Shared session-name formatting for CLI and RPC session auto-naming.
|
|
4
|
+
module SessionNaming
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def default_name(input)
|
|
8
|
+
input.to_s.gsub(/\s+/, " ").strip.slice(0, 120).to_s
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|