kward 0.74.0 → 0.76.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/CHANGELOG.md +47 -0
- data/Gemfile.lock +2 -2
- data/README.md +4 -2
- data/Rakefile +18 -0
- data/doc/configuration.md +75 -10
- data/doc/extensibility.md +6 -19
- data/doc/getting-started.md +1 -1
- data/doc/lifecycle-hooks.md +424 -0
- data/doc/memory.md +1 -1
- data/doc/personas.md +2 -2
- data/doc/plugins.md +2 -1
- data/doc/releasing.md +7 -15
- data/doc/rpc.md +111 -16
- data/doc/session-management.md +9 -9
- data/doc/skills.md +199 -0
- data/doc/usage.md +7 -7
- data/lib/kward/agent.rb +77 -22
- data/lib/kward/auth/anthropic_oauth.rb +5 -87
- data/lib/kward/auth/oauth_helpers.rb +105 -0
- data/lib/kward/auth/openai_oauth.rb +4 -86
- data/lib/kward/cli/commands.rb +9 -2
- data/lib/kward/cli/compaction.rb +7 -0
- data/lib/kward/cli/git.rb +30 -3
- data/lib/kward/cli/hook_commands.rb +258 -0
- data/lib/kward/cli/interactive_turn.rb +3 -0
- data/lib/kward/cli/memory_commands.rb +1 -5
- data/lib/kward/cli/plugins.rb +41 -1
- data/lib/kward/cli/prompt_interface.rb +28 -7
- data/lib/kward/cli/rendering.rb +8 -4
- data/lib/kward/cli/runtime_helpers.rb +44 -5
- data/lib/kward/cli/sessions.rb +13 -0
- data/lib/kward/cli/settings.rb +226 -152
- data/lib/kward/cli/slash_commands.rb +81 -42
- data/lib/kward/cli/tabs.rb +26 -6
- data/lib/kward/cli.rb +26 -3
- data/lib/kward/cli_transcript_formatter.rb +21 -18
- data/lib/kward/compaction/token_estimator.rb +112 -0
- data/lib/kward/compactor.rb +42 -116
- data/lib/kward/config_files.rb +120 -25
- data/lib/kward/deep_copy.rb +42 -0
- data/lib/kward/diff_view_mode.rb +36 -0
- data/lib/kward/events.rb +2 -0
- data/lib/kward/export_path.rb +2 -6
- data/lib/kward/frontmatter.rb +43 -0
- data/lib/kward/hooks/audit_log.rb +121 -0
- data/lib/kward/hooks/catalog.rb +110 -0
- data/lib/kward/hooks/command_handler.rb +66 -0
- data/lib/kward/hooks/config_loader.rb +104 -0
- data/lib/kward/hooks/decision.rb +102 -0
- data/lib/kward/hooks/event.rb +60 -0
- data/lib/kward/hooks/http_handler.rb +72 -0
- data/lib/kward/hooks/manager.rb +158 -0
- data/lib/kward/hooks/matcher.rb +75 -0
- data/lib/kward/hooks.rb +16 -0
- data/lib/kward/markdown_transcript.rb +4 -3
- data/lib/kward/mcp/client.rb +1 -1
- data/lib/kward/mcp/server_config.rb +7 -3
- data/lib/kward/mcp/stdio_transport.rb +7 -1
- data/lib/kward/memory/turn_context.rb +19 -0
- data/lib/kward/message_access.rb +2 -2
- data/lib/kward/message_text.rb +19 -0
- data/lib/kward/model/client.rb +23 -52
- data/lib/kward/model/context_usage.rb +10 -4
- data/lib/kward/model/copilot_models.rb +44 -0
- data/lib/kward/model/model_info.rb +38 -5
- data/lib/kward/model/payloads.rb +15 -17
- data/lib/kward/model/stream_parser.rb +183 -18
- data/lib/kward/pan/index.html.erb +1 -0
- data/lib/kward/pan/server.rb +53 -2
- data/lib/kward/path_guard.rb +13 -0
- data/lib/kward/plugin_registry.rb +88 -25
- data/lib/kward/prompt_interface/editor/controller.rb +60 -1
- data/lib/kward/prompt_interface/editor/renderer.rb +11 -0
- data/lib/kward/prompt_interface/editor/state.rb +5 -1
- data/lib/kward/prompt_interface/selection_prompt.rb +26 -6
- data/lib/kward/prompt_interface/transcript_renderer.rb +1 -1
- data/lib/kward/prompt_interface.rb +4 -1
- data/lib/kward/prompts/commands.rb +3 -3
- data/lib/kward/prompts.rb +7 -7
- data/lib/kward/rpc/mcp_status.rb +50 -0
- data/lib/kward/rpc/prompt_bridge.rb +38 -4
- data/lib/kward/rpc/runtime_payloads.rb +1 -1
- data/lib/kward/rpc/server.rb +95 -15
- data/lib/kward/rpc/session_manager.rb +260 -33
- data/lib/kward/rpc/session_metrics.rb +1 -1
- data/lib/kward/rpc/session_tree_rows.rb +13 -64
- data/lib/kward/rpc/tool_event_normalizer.rb +16 -2
- data/lib/kward/rpc/transcript_normalizer.rb +19 -20
- data/lib/kward/rpc/turn_context.rb +109 -0
- data/lib/kward/session_store.rb +6 -20
- data/lib/kward/session_tree_nodes.rb +98 -18
- data/lib/kward/session_tree_renderer.rb +6 -61
- data/lib/kward/skills/registry.rb +109 -17
- data/lib/kward/tools/mcp_tool.rb +4 -0
- data/lib/kward/tools/read_skill.rb +9 -4
- data/lib/kward/tools/registry.rb +266 -8
- data/lib/kward/tools/search/code.rb +4 -22
- data/lib/kward/tools/search/web.rb +28 -2
- data/lib/kward/update_check.rb +122 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/manager.rb +43 -3
- data/lib/kward/workspace.rb +3 -1
- data/templates/default/fulldoc/html/css/kward.css +22 -0
- data/templates/default/fulldoc/html/js/kward.js +2 -0
- data/templates/default/kward_navigation.rb +4 -2
- data/templates/default/layout/html/setup.rb +2 -0
- metadata +25 -1
|
@@ -19,6 +19,12 @@ module Kward
|
|
|
19
19
|
activity = memory_summarize_command?(argument) ? "summarizing" : "loading"
|
|
20
20
|
run_busy_local_command_and_requeue(activity: activity) { handle_memory_command(argument, agent) }
|
|
21
21
|
[true, nil]
|
|
22
|
+
when "hooks"
|
|
23
|
+
run_busy_local_command_and_requeue { handle_hooks_command(argument) }
|
|
24
|
+
[true, nil]
|
|
25
|
+
when "skill"
|
|
26
|
+
run_busy_local_command_and_requeue { activate_skill_command(argument, agent) }
|
|
27
|
+
[true, nil]
|
|
22
28
|
when "redraw"
|
|
23
29
|
run_busy_local_command_and_requeue { @prompt.redraw if @prompt.respond_to?(:redraw) }
|
|
24
30
|
[true, nil]
|
|
@@ -74,38 +80,16 @@ module Kward
|
|
|
74
80
|
[true, nil]
|
|
75
81
|
when "new"
|
|
76
82
|
[true, run_busy_local_command_and_requeue { start_new_session(session_store) }]
|
|
77
|
-
when "
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if path.empty?
|
|
85
|
-
sessions = run_busy_local_command_and_requeue { session_store.recent_tree(limit: nil) }
|
|
86
|
-
path = select_session_path_from_sessions(sessions, session_store: session_store)
|
|
87
|
-
end
|
|
88
|
-
replacement_agent = nil
|
|
89
|
-
selection = path
|
|
90
|
-
loop do
|
|
91
|
-
replacement_agent = if selection.respond_to?(:conversation)
|
|
92
|
-
selection
|
|
93
|
-
elsif selection.is_a?(Hash) && selection[:action] == :clone
|
|
94
|
-
run_busy_local_command_and_requeue(activity: "cloning") { clone_session_from_path(session_store, selection[:path]) }
|
|
95
|
-
elsif selection.is_a?(Hash) && selection[:action] == :fork
|
|
96
|
-
selection = reopen_sessions_after_fork(session_store, selection[:path], selection[:choice_label])
|
|
97
|
-
next
|
|
98
|
-
elsif selection.to_s.empty?
|
|
99
|
-
nil
|
|
100
|
-
else
|
|
101
|
-
run_busy_local_command_and_requeue { resume_session(session_store, selection) }
|
|
102
|
-
end
|
|
103
|
-
break
|
|
83
|
+
when "session"
|
|
84
|
+
session_command, session_argument = argument.to_s.split(/\s+/, 2)
|
|
85
|
+
if session_command == "name"
|
|
86
|
+
rename_session(session_argument)
|
|
87
|
+
[true, nil]
|
|
88
|
+
else
|
|
89
|
+
[true, open_or_resume_session(session_store, argument)]
|
|
104
90
|
end
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
rename_session(argument)
|
|
108
|
-
[true, nil]
|
|
91
|
+
when "resume"
|
|
92
|
+
[true, open_or_resume_session(session_store, argument)]
|
|
109
93
|
when "rename"
|
|
110
94
|
rename_session(argument, require_name: true)
|
|
111
95
|
[true, nil]
|
|
@@ -127,7 +111,10 @@ module Kward
|
|
|
127
111
|
run_busy_local_command_and_requeue(activity: "compacting") { compact_context(agent, argument) }
|
|
128
112
|
[true, nil]
|
|
129
113
|
else
|
|
130
|
-
if
|
|
114
|
+
if name.to_s.start_with?("skill:")
|
|
115
|
+
run_busy_local_command_and_requeue { activate_skill_command(name.to_s.delete_prefix("skill:"), agent) }
|
|
116
|
+
[true, nil]
|
|
117
|
+
elsif interactive_command_for(name) && prompt_interface? && @prompt.respond_to?(:start_interactive)
|
|
131
118
|
run_interactive_command(name, argument, agent)
|
|
132
119
|
elsif plugin_command_for(name)
|
|
133
120
|
run_busy_local_command_and_requeue(activity: "running") { run_plugin_command(name, argument, agent) }
|
|
@@ -141,6 +128,56 @@ module Kward
|
|
|
141
128
|
PromptCommands.parse(command) || [nil, ""]
|
|
142
129
|
end
|
|
143
130
|
|
|
131
|
+
def open_or_resume_session(session_store, argument)
|
|
132
|
+
unless session_store
|
|
133
|
+
say_sessions_unavailable
|
|
134
|
+
return nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
path = argument.to_s.strip
|
|
138
|
+
if path.empty?
|
|
139
|
+
sessions = run_busy_local_command_and_requeue { session_store.recent_tree(limit: nil) }
|
|
140
|
+
path = select_session_path_from_sessions(sessions, session_store: session_store)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
selection = path
|
|
144
|
+
loop do
|
|
145
|
+
replacement_agent = if selection.respond_to?(:conversation)
|
|
146
|
+
selection
|
|
147
|
+
elsif selection.is_a?(Hash) && selection[:action] == :clone
|
|
148
|
+
run_busy_local_command_and_requeue(activity: "cloning") { clone_session_from_path(session_store, selection[:path]) }
|
|
149
|
+
elsif selection.is_a?(Hash) && selection[:action] == :fork
|
|
150
|
+
selection = reopen_sessions_after_fork(session_store, selection[:path], selection[:choice_label])
|
|
151
|
+
next
|
|
152
|
+
elsif selection.to_s.empty?
|
|
153
|
+
nil
|
|
154
|
+
else
|
|
155
|
+
run_busy_local_command_and_requeue { resume_session(session_store, selection) }
|
|
156
|
+
end
|
|
157
|
+
return replacement_agent
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def activate_skill_command(name, agent)
|
|
162
|
+
skill_name = name.to_s.strip
|
|
163
|
+
if skill_name.empty?
|
|
164
|
+
runtime_output("Usage: /skill <name>")
|
|
165
|
+
return
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
tool_call = {
|
|
169
|
+
"id" => "skill_#{skill_name.gsub(/[^a-zA-Z0-9_-]/, "_")}",
|
|
170
|
+
"type" => "function",
|
|
171
|
+
"function" => {
|
|
172
|
+
"name" => "read_skill",
|
|
173
|
+
"arguments" => JSON.dump({ name: skill_name })
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
agent.conversation.append_assistant("role" => "assistant", "content" => nil, "tool_calls" => [tool_call])
|
|
177
|
+
result = agent.tool_registry.dispatch(tool_call, agent.conversation)
|
|
178
|
+
runtime_output(result.start_with?("Error:") ? result : "Activated skill: #{skill_name}")
|
|
179
|
+
end
|
|
180
|
+
|
|
144
181
|
def open_session_diff
|
|
145
182
|
unless @active_session&.path
|
|
146
183
|
runtime_output("No active persisted session.")
|
|
@@ -413,7 +450,9 @@ module Kward
|
|
|
413
450
|
reasoning_effort: current_reasoning_effort,
|
|
414
451
|
write_lock: (@worker_write_lock ||= Workers::WriteLock.new),
|
|
415
452
|
worker_store: worker_store,
|
|
416
|
-
write_lane_available: -> { !@foreground_turn_active }
|
|
453
|
+
write_lane_available: -> { !@foreground_turn_active },
|
|
454
|
+
hook_manager: lifecycle_hook_manager(agent.conversation),
|
|
455
|
+
hook_context: lifecycle_hook_context(agent.conversation)
|
|
417
456
|
)
|
|
418
457
|
end
|
|
419
458
|
|
|
@@ -452,7 +491,7 @@ module Kward
|
|
|
452
491
|
def open_worker_list(agent, session_store, title: "Workers", empty_message: "No workers in the pipeline.")
|
|
453
492
|
return runtime_output(empty_message) unless @prompt.respond_to?(:select)
|
|
454
493
|
|
|
455
|
-
jobs = worker_jobs
|
|
494
|
+
jobs = worker_jobs
|
|
456
495
|
if jobs.empty?
|
|
457
496
|
runtime_output(empty_message)
|
|
458
497
|
return
|
|
@@ -466,15 +505,15 @@ module Kward
|
|
|
466
505
|
open_worker_actions(selected, agent, session_store) if selected
|
|
467
506
|
end
|
|
468
507
|
|
|
469
|
-
def worker_jobs
|
|
508
|
+
def worker_jobs
|
|
470
509
|
runtime_worker_ids = @worker_manager ? @worker_manager.list.map(&:id) : []
|
|
471
510
|
persisted_workers = worker_store.list.reject { |job| runtime_worker_ids.include?(job["id"]) }
|
|
472
511
|
live_workers = @worker_manager ? @worker_manager.list.map(&:to_h) : []
|
|
473
|
-
[implementation_worker_job
|
|
512
|
+
[implementation_worker_job].compact + persisted_workers + live_workers
|
|
474
513
|
end
|
|
475
514
|
|
|
476
|
-
def implementation_worker_job
|
|
477
|
-
remember_implementation_worker
|
|
515
|
+
def implementation_worker_job
|
|
516
|
+
remember_implementation_worker if implementation_agent?
|
|
478
517
|
path = @implementation_worker_session_path || @active_session&.path
|
|
479
518
|
return nil if path.to_s.empty?
|
|
480
519
|
|
|
@@ -482,18 +521,18 @@ module Kward
|
|
|
482
521
|
"id" => "implementation",
|
|
483
522
|
"title" => @implementation_worker_title || @active_session&.name || "Implementation",
|
|
484
523
|
"role" => "implementation",
|
|
485
|
-
"status" => implementation_agent?
|
|
524
|
+
"status" => implementation_agent? ? "active" : "idle",
|
|
486
525
|
"session_path" => path
|
|
487
526
|
}
|
|
488
527
|
end
|
|
489
528
|
|
|
490
|
-
def implementation_agent?
|
|
529
|
+
def implementation_agent?
|
|
491
530
|
@active_worker_role.to_s.empty? || @active_worker_role == "implementation"
|
|
492
531
|
end
|
|
493
532
|
|
|
494
|
-
def remember_implementation_worker
|
|
533
|
+
def remember_implementation_worker
|
|
495
534
|
return unless @active_session&.path
|
|
496
|
-
return unless implementation_agent?
|
|
535
|
+
return unless implementation_agent?
|
|
497
536
|
|
|
498
537
|
@implementation_worker_session_path = @active_session.path
|
|
499
538
|
@implementation_worker_title = @active_session.name || "Implementation"
|
data/lib/kward/cli/tabs.rb
CHANGED
|
@@ -28,14 +28,19 @@ module Kward
|
|
|
28
28
|
:pending_question,
|
|
29
29
|
:shell,
|
|
30
30
|
:error_reported,
|
|
31
|
+
:local_busy_activity,
|
|
31
32
|
keyword_init: true
|
|
32
33
|
) do
|
|
33
34
|
def running?
|
|
34
35
|
%w[queued running waiting_for_question].include?(status.to_s)
|
|
35
36
|
end
|
|
36
37
|
|
|
38
|
+
def local_busy?
|
|
39
|
+
!local_busy_activity.to_s.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
37
42
|
def idle?
|
|
38
|
-
!running?
|
|
43
|
+
!running? && !local_busy?
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
def record_event(event)
|
|
@@ -121,10 +126,23 @@ module Kward
|
|
|
121
126
|
conversation.plugin_registry ||= plugin_registry if conversation.respond_to?(:plugin_registry)
|
|
122
127
|
workspace = configured_workspace(root: conversation.workspace_root)
|
|
123
128
|
prompt = TabQuestionPrompt.new(self)
|
|
124
|
-
|
|
129
|
+
hook_manager = lifecycle_hook_manager(conversation)
|
|
130
|
+
hook_context = lifecycle_hook_context(conversation)
|
|
131
|
+
tool_registry = ToolRegistry.new(
|
|
132
|
+
workspace: workspace,
|
|
133
|
+
prompt: prompt,
|
|
134
|
+
hook_manager: hook_manager,
|
|
135
|
+
hook_context: hook_context
|
|
136
|
+
)
|
|
125
137
|
@footer_conversation = conversation
|
|
126
138
|
@footer_tool_registry = tool_registry
|
|
127
|
-
agent = Agent.new(
|
|
139
|
+
agent = Agent.new(
|
|
140
|
+
client: @client,
|
|
141
|
+
tool_registry: tool_registry,
|
|
142
|
+
conversation: conversation,
|
|
143
|
+
hook_manager: hook_manager,
|
|
144
|
+
hook_context: hook_context
|
|
145
|
+
)
|
|
128
146
|
agent.instance_variable_set(:@tab_question_prompt, prompt)
|
|
129
147
|
agent
|
|
130
148
|
end
|
|
@@ -150,7 +168,8 @@ module Kward
|
|
|
150
168
|
unread: false,
|
|
151
169
|
pending_question: nil,
|
|
152
170
|
shell: nil,
|
|
153
|
-
error_reported: false
|
|
171
|
+
error_reported: false,
|
|
172
|
+
local_busy_activity: nil
|
|
154
173
|
).tap { |tab| assign_tab_question_prompt(agent, tab) }
|
|
155
174
|
end
|
|
156
175
|
|
|
@@ -231,7 +250,7 @@ module Kward
|
|
|
231
250
|
|
|
232
251
|
def close_active_tab
|
|
233
252
|
tab = active_tab
|
|
234
|
-
if tab&.running?
|
|
253
|
+
if tab&.running? || tab&.local_busy?
|
|
235
254
|
runtime_output("Tab #{active_tab_number} is running and cannot be closed yet.")
|
|
236
255
|
return nil
|
|
237
256
|
end
|
|
@@ -312,6 +331,7 @@ module Kward
|
|
|
312
331
|
update_prompt_tabs
|
|
313
332
|
render_tab(tab) if render
|
|
314
333
|
start_tab_live_view(tab) if tab.running?
|
|
334
|
+
@prompt.begin_busy_input("You>", activity: tab.local_busy_activity) if tab.local_busy? && @prompt.respond_to?(:begin_busy_input)
|
|
315
335
|
persist_tabs
|
|
316
336
|
service_active_tab_question
|
|
317
337
|
tab.agent
|
|
@@ -598,7 +618,7 @@ module Kward
|
|
|
598
618
|
|
|
599
619
|
def tab_label_color(tab)
|
|
600
620
|
return :green if tab.status.to_s == "waiting_for_question"
|
|
601
|
-
return :yellow if tab.running?
|
|
621
|
+
return :yellow if tab.running? || tab.local_busy?
|
|
602
622
|
return :red if %w[failed cancelled].include?(tab.status.to_s)
|
|
603
623
|
return :green if tab.unread
|
|
604
624
|
|
data/lib/kward/cli.rb
CHANGED
|
@@ -21,6 +21,7 @@ require_relative "auth/github_oauth"
|
|
|
21
21
|
require_relative "auth/openrouter_api_key"
|
|
22
22
|
require_relative "image_attachments"
|
|
23
23
|
require_relative "memory/manager"
|
|
24
|
+
require_relative "memory/turn_context"
|
|
24
25
|
require_relative "transcript_export"
|
|
25
26
|
require_relative "message_access"
|
|
26
27
|
require_relative "model/model_info"
|
|
@@ -38,6 +39,7 @@ require_relative "session_trash"
|
|
|
38
39
|
require_relative "session_tree_renderer"
|
|
39
40
|
require_relative "starter_pack_installer"
|
|
40
41
|
require_relative "steering"
|
|
42
|
+
require_relative "update_check"
|
|
41
43
|
require_relative "workers"
|
|
42
44
|
require_relative "tools/tool_call"
|
|
43
45
|
require_relative "tools/registry"
|
|
@@ -52,6 +54,7 @@ require_relative "cli/openrouter_commands"
|
|
|
52
54
|
require_relative "cli/runtime_helpers"
|
|
53
55
|
require_relative "cli/slash_commands"
|
|
54
56
|
require_relative "cli/memory_commands"
|
|
57
|
+
require_relative "cli/hook_commands"
|
|
55
58
|
require_relative "cli/settings"
|
|
56
59
|
require_relative "cli/sessions"
|
|
57
60
|
require_relative "cli/tabs"
|
|
@@ -66,7 +69,7 @@ require_relative "cli/tool_summaries"
|
|
|
66
69
|
# Namespace for the Kward CLI agent runtime.
|
|
67
70
|
module Kward
|
|
68
71
|
# Command-line interface for interactive chat, one-shot prompts, login,
|
|
69
|
-
# telemetry export, Pan server mode, and the
|
|
72
|
+
# telemetry export, Pan server mode, and the JSON-RPC backend.
|
|
70
73
|
class CLI
|
|
71
74
|
RESTORED_TOOL_OUTPUT_LIMIT = 2_000
|
|
72
75
|
INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT = 10
|
|
@@ -84,6 +87,7 @@ module Kward
|
|
|
84
87
|
include CLI::RuntimeHelpers
|
|
85
88
|
include CLI::SlashCommands
|
|
86
89
|
include CLI::MemoryCommands
|
|
90
|
+
include CLI::HookCommands
|
|
87
91
|
include CLI::Settings
|
|
88
92
|
include CLI::Sessions
|
|
89
93
|
include CLI::Tabs
|
|
@@ -209,6 +213,16 @@ module Kward
|
|
|
209
213
|
return
|
|
210
214
|
end
|
|
211
215
|
|
|
216
|
+
if @argv.first == "hooks"
|
|
217
|
+
if help_option_arguments?(@argv[1..] || [])
|
|
218
|
+
print_command_help("hooks")
|
|
219
|
+
return
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
handle_hooks_command((@argv[1..] || []).join(" "))
|
|
223
|
+
return
|
|
224
|
+
end
|
|
225
|
+
|
|
212
226
|
if @argv.first == "edit"
|
|
213
227
|
if help_option_arguments?(@argv[1..] || [])
|
|
214
228
|
print_command_help("edit")
|
|
@@ -328,10 +342,19 @@ module Kward
|
|
|
328
342
|
markdown_chunks = []
|
|
329
343
|
conversation = new_conversation
|
|
330
344
|
apply_filter_system_prompt(conversation) if filter
|
|
345
|
+
hook_manager = lifecycle_hook_manager(conversation)
|
|
346
|
+
hook_context = lifecycle_hook_context(conversation)
|
|
331
347
|
agent = Agent.new(
|
|
332
348
|
client: @client,
|
|
333
|
-
tool_registry: ToolRegistry.new(
|
|
334
|
-
|
|
349
|
+
tool_registry: ToolRegistry.new(
|
|
350
|
+
workspace: configured_workspace,
|
|
351
|
+
prompt: @prompt,
|
|
352
|
+
hook_manager: hook_manager,
|
|
353
|
+
hook_context: hook_context
|
|
354
|
+
),
|
|
355
|
+
conversation: conversation,
|
|
356
|
+
hook_manager: hook_manager,
|
|
357
|
+
hook_context: hook_context
|
|
335
358
|
)
|
|
336
359
|
answer = if filter
|
|
337
360
|
agent.ask(input)
|
|
@@ -10,25 +10,31 @@ module Kward
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
12
|
def reasoning(message)
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
reasoning_blocks(message).join("\n")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reasoning_blocks(message)
|
|
17
|
+
response_blocks = response_items(message).flat_map do |item|
|
|
18
|
+
next [] unless MessageAccess.value(item, :type) == "reasoning"
|
|
19
|
+
|
|
20
|
+
summary = response_item_text_parts(MessageAccess.value(item, :summary))
|
|
21
|
+
summary.empty? ? response_item_text_parts(MessageAccess.value(item, :content)) : summary
|
|
22
|
+
end
|
|
23
|
+
return response_blocks unless response_blocks.empty?
|
|
15
24
|
|
|
16
25
|
content = MessageAccess.content(message)
|
|
17
26
|
if content.is_a?(Array)
|
|
18
|
-
|
|
27
|
+
blocks = content.filter_map do |part|
|
|
19
28
|
type = MessageAccess.value(part, :type)
|
|
20
29
|
next unless ["thinking", "reasoning"].include?(type)
|
|
21
30
|
|
|
22
31
|
MessageAccess.value(part, :thinking) || MessageAccess.value(part, :reasoning) || MessageAccess.value(part, :text)
|
|
23
|
-
end.
|
|
24
|
-
return
|
|
32
|
+
end.map(&:to_s).reject(&:empty?)
|
|
33
|
+
return blocks unless blocks.empty?
|
|
25
34
|
end
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
response_item_text(MessageAccess.value(item, :summary)).empty? ? response_item_text(MessageAccess.value(item, :content)) : response_item_text(MessageAccess.value(item, :summary))
|
|
31
|
-
end.join("\n")
|
|
36
|
+
direct = MessageAccess.value(message, :reasoning_summary).to_s
|
|
37
|
+
direct.empty? ? [] : [direct]
|
|
32
38
|
end
|
|
33
39
|
|
|
34
40
|
def content_text(content)
|
|
@@ -52,13 +58,6 @@ module Kward
|
|
|
52
58
|
end.join
|
|
53
59
|
end
|
|
54
60
|
|
|
55
|
-
def display_text(message)
|
|
56
|
-
display_content = MessageAccess.display_content(message)
|
|
57
|
-
return display_content.to_s unless display_content.nil?
|
|
58
|
-
|
|
59
|
-
content_text(MessageAccess.content(message))
|
|
60
|
-
end
|
|
61
|
-
|
|
62
61
|
def user_display_text(message)
|
|
63
62
|
display_content = MessageAccess.display_content(message)
|
|
64
63
|
return display_content.to_s unless display_content.nil?
|
|
@@ -120,11 +119,15 @@ module Kward
|
|
|
120
119
|
end
|
|
121
120
|
|
|
122
121
|
def response_item_text(parts)
|
|
122
|
+
response_item_text_parts(parts).join
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def response_item_text_parts(parts)
|
|
123
126
|
Array(parts).filter_map do |part|
|
|
124
127
|
next unless part.is_a?(Hash)
|
|
125
128
|
|
|
126
129
|
MessageAccess.value(part, :text) || MessageAccess.value(part, :refusal)
|
|
127
|
-
end.
|
|
130
|
+
end.map(&:to_s).reject(&:empty?)
|
|
128
131
|
end
|
|
129
132
|
|
|
130
133
|
def image_part_reference(part)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "../message_access"
|
|
3
|
+
require_relative "../tools/tool_call"
|
|
4
|
+
|
|
5
|
+
# Namespace for the Kward CLI agent runtime.
|
|
6
|
+
module Kward
|
|
7
|
+
# Conversation compaction settings, planning, and summary generation.
|
|
8
|
+
module Compaction
|
|
9
|
+
# Estimates transcript token usage from messages and provider usage metadata.
|
|
10
|
+
class TokenEstimator
|
|
11
|
+
def estimate_tokens(text)
|
|
12
|
+
(text.to_s.length / 4.0).ceil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def messages_tokens(messages)
|
|
16
|
+
Array(messages).sum { |message| message_tokens(message) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def context_tokens(messages)
|
|
20
|
+
messages = Array(messages)
|
|
21
|
+
usage_info = last_assistant_usage_info(messages)
|
|
22
|
+
return messages_tokens(messages) unless usage_info
|
|
23
|
+
|
|
24
|
+
usage_tokens = usage_tokens(usage_info[:usage])
|
|
25
|
+
trailing_tokens = messages[(usage_info[:index] + 1)..].to_a.sum { |message| message_tokens(message) }
|
|
26
|
+
usage_tokens + trailing_tokens
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def message_tokens(message)
|
|
30
|
+
role = value(message, :role)
|
|
31
|
+
parts = [role]
|
|
32
|
+
if role.to_s == "compactionSummary"
|
|
33
|
+
parts << value(message, :summary)
|
|
34
|
+
else
|
|
35
|
+
parts << content_text(value(message, :content))
|
|
36
|
+
end
|
|
37
|
+
parts << value(message, :reasoning_summary)
|
|
38
|
+
tool_calls(message).each do |tool_call|
|
|
39
|
+
parts << tool_call_name(tool_call)
|
|
40
|
+
parts << tool_call_arguments(tool_call)
|
|
41
|
+
end
|
|
42
|
+
parts << value(message, :tool_call_id)
|
|
43
|
+
parts << value(message, :name)
|
|
44
|
+
estimate_tokens(parts.compact.join("\n"))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def content_text(content)
|
|
50
|
+
return content.to_s unless content.is_a?(Array)
|
|
51
|
+
|
|
52
|
+
content.filter_map do |part|
|
|
53
|
+
type = value(part, :type)
|
|
54
|
+
if type == "text"
|
|
55
|
+
value(part, :text)
|
|
56
|
+
elsif type == "image"
|
|
57
|
+
path = value(part, :path)
|
|
58
|
+
media_type = value(part, :media_type) || value(part, :mimeType) || "image"
|
|
59
|
+
"[#{media_type}#{path ? ": #{path}" : ""}]"
|
|
60
|
+
end
|
|
61
|
+
end.join("\n")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def tool_calls(message)
|
|
65
|
+
MessageAccess.tool_calls(message)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def tool_call_name(tool_call)
|
|
69
|
+
ToolCall.name(tool_call)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def tool_call_arguments(tool_call)
|
|
73
|
+
arguments = ToolCall.raw_arguments(tool_call)
|
|
74
|
+
arguments.is_a?(Hash) ? JSON.dump(arguments) : arguments.to_s
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def last_assistant_usage_info(messages)
|
|
78
|
+
messages.each_with_index.reverse_each do |message, index|
|
|
79
|
+
next unless value(message, :role).to_s == "assistant"
|
|
80
|
+
|
|
81
|
+
usage = value(message, :usage)
|
|
82
|
+
tokens = usage_tokens(usage)
|
|
83
|
+
return { usage: usage, index: index } if tokens.positive?
|
|
84
|
+
end
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def usage_tokens(usage)
|
|
89
|
+
return 0 unless usage.respond_to?(:key?)
|
|
90
|
+
|
|
91
|
+
total = usage_value(usage, :total_tokens, "totalTokens")
|
|
92
|
+
return total if total.positive?
|
|
93
|
+
|
|
94
|
+
usage_value(usage, :input_tokens, "input", "prompt_tokens") +
|
|
95
|
+
usage_value(usage, :output_tokens, "output", "completion_tokens") +
|
|
96
|
+
usage_value(usage, :cache_read_tokens, "cacheRead", "cacheReadTokens", "cache_read", "cached_tokens") +
|
|
97
|
+
usage_value(usage, :cache_write_tokens, "cacheWrite", "cacheWriteTokens", "cache_write")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def usage_value(usage, *keys)
|
|
101
|
+
key = keys.find { |candidate| usage.key?(candidate) || usage.key?(candidate.to_s) }
|
|
102
|
+
return 0 unless key
|
|
103
|
+
|
|
104
|
+
(usage[key] || usage[key.to_s]).to_i
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def value(object, key)
|
|
108
|
+
ToolCall.value(object, key)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|