kward 0.67.0 → 0.68.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 +26 -0
- data/Gemfile.lock +2 -2
- data/README.md +5 -5
- data/doc/authentication.md +24 -1
- data/doc/configuration.md +9 -2
- data/doc/extensibility.md +1 -1
- data/doc/getting-started.md +4 -6
- data/doc/plugins.md +0 -2
- data/doc/releasing.md +7 -8
- data/doc/rpc.md +6 -6
- data/doc/usage.md +5 -2
- data/doc/web-search.md +2 -2
- data/kward.gemspec +4 -0
- data/lib/kward/agent.rb +29 -2
- data/lib/kward/ansi.rb +3 -0
- data/lib/kward/auth/anthropic_oauth.rb +291 -0
- data/lib/kward/auth/file.rb +2 -0
- data/lib/kward/auth/github_oauth.rb +3 -0
- data/lib/kward/auth/openai_oauth.rb +4 -0
- data/lib/kward/auth/openrouter_api_key.rb +2 -0
- data/lib/kward/cancellation.rb +3 -0
- data/lib/kward/cli/auth_commands.rb +82 -0
- data/lib/kward/cli/commands.rb +222 -0
- data/lib/kward/cli/compaction.rb +25 -0
- data/lib/kward/cli/doctor.rb +121 -0
- data/lib/kward/cli/interactive_turn.rb +225 -0
- data/lib/kward/cli/memory_commands.rb +133 -0
- data/lib/kward/cli/plugins.rb +112 -0
- data/lib/kward/cli/prompt_interface.rb +132 -0
- data/lib/kward/cli/rendering.rb +389 -0
- data/lib/kward/cli/runtime_helpers.rb +159 -0
- data/lib/kward/cli/sessions.rb +376 -0
- data/lib/kward/cli/settings.rb +663 -0
- data/lib/kward/cli/slash_commands.rb +112 -0
- data/lib/kward/cli/stats.rb +64 -0
- data/lib/kward/cli/tool_summaries.rb +153 -0
- data/lib/kward/cli.rb +38 -2790
- data/lib/kward/cli_transcript_formatter.rb +4 -7
- data/lib/kward/clipboard.rb +1 -0
- data/lib/kward/compaction/file_operation_tracker.rb +3 -0
- data/lib/kward/compactor.rb +29 -7
- data/lib/kward/config_files.rb +33 -24
- data/lib/kward/conversation.rb +70 -5
- data/lib/kward/events.rb +2 -0
- data/lib/kward/export_path.rb +2 -0
- data/lib/kward/image_attachments.rb +2 -0
- data/lib/kward/markdown_transcript.rb +2 -0
- data/lib/kward/memory/manager.rb +13 -0
- data/lib/kward/message_access.rb +23 -2
- data/lib/kward/message_text.rb +45 -0
- data/lib/kward/model/chat_invocation.rb +2 -0
- data/lib/kward/model/client.rb +295 -77
- data/lib/kward/model/context_overflow.rb +2 -0
- data/lib/kward/model/context_usage.rb +3 -0
- data/lib/kward/model/model_info.rb +143 -4
- data/lib/kward/model/payloads.rb +166 -13
- data/lib/kward/model/retry_message.rb +2 -0
- data/lib/kward/model/stream_parser.rb +129 -0
- data/lib/kward/pan/server.rb +3 -1
- data/lib/kward/plugin_registry.rb +12 -0
- data/lib/kward/private_file.rb +2 -0
- data/lib/kward/prompt_interface/banner.rb +3 -0
- data/lib/kward/prompt_interface/composer_controller.rb +262 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +172 -0
- data/lib/kward/prompt_interface/composer_state.rb +221 -0
- data/lib/kward/prompt_interface/key_handler.rb +365 -0
- data/lib/kward/prompt_interface/layout.rb +31 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +111 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +91 -0
- data/lib/kward/prompt_interface/question_prompt.rb +328 -0
- data/lib/kward/prompt_interface/runtime_state.rb +59 -0
- data/lib/kward/prompt_interface/screen.rb +186 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +242 -0
- data/lib/kward/prompt_interface/slash_overlay.rb +102 -0
- data/lib/kward/prompt_interface/stream_state.rb +65 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +85 -0
- data/lib/kward/prompt_interface/transcript_renderer.rb +142 -0
- data/lib/kward/prompt_interface.rb +69 -1832
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts/templates.rb +3 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/question_contract.rb +66 -0
- data/lib/kward/resources/avatar_kward_logo.rb +2 -0
- data/lib/kward/resources/pixel_logo.rb +2 -0
- data/lib/kward/rpc/attachment_normalizer.rb +60 -0
- data/lib/kward/rpc/auth_manager.rb +65 -11
- data/lib/kward/rpc/config_manager.rb +11 -0
- data/lib/kward/rpc/prompt_bridge.rb +5 -26
- data/lib/kward/rpc/redactor.rb +3 -0
- data/lib/kward/rpc/runtime_payloads.rb +4 -1
- data/lib/kward/rpc/server.rb +37 -10
- data/lib/kward/rpc/session_manager.rb +123 -347
- data/lib/kward/rpc/session_metrics.rb +68 -0
- data/lib/kward/rpc/session_tree.rb +48 -0
- data/lib/kward/rpc/session_tree_rows.rb +208 -0
- data/lib/kward/rpc/tool_event_normalizer.rb +3 -0
- data/lib/kward/rpc/tool_metadata.rb +3 -0
- data/lib/kward/rpc/transcript_normalizer.rb +3 -0
- data/lib/kward/rpc/transport.rb +3 -0
- data/lib/kward/session_diff.rb +2 -0
- data/lib/kward/session_store.rb +125 -31
- data/lib/kward/session_trash.rb +1 -0
- data/lib/kward/session_tree_renderer.rb +8 -41
- data/lib/kward/session_tree_tool_display.rb +56 -0
- data/lib/kward/skills/registry.rb +3 -0
- data/lib/kward/starter_pack_installer.rb +1 -0
- data/lib/kward/steering.rb +2 -0
- data/lib/kward/telemetry/logger.rb +3 -0
- data/lib/kward/telemetry/stats.rb +3 -0
- data/lib/kward/tools/ask_user_question.rb +20 -32
- data/lib/kward/tools/base.rb +8 -0
- data/lib/kward/tools/code_search.rb +5 -0
- data/lib/kward/tools/edit_file.rb +5 -0
- data/lib/kward/tools/list_directory.rb +5 -0
- data/lib/kward/tools/read_file.rb +5 -0
- data/lib/kward/tools/read_skill.rb +5 -0
- data/lib/kward/tools/registry.rb +33 -2
- data/lib/kward/tools/run_shell_command.rb +5 -0
- data/lib/kward/tools/search/code.rb +7 -0
- data/lib/kward/tools/search/web.rb +17 -14
- data/lib/kward/tools/tool_call.rb +25 -5
- data/lib/kward/tools/web_search.rb +7 -1
- data/lib/kward/tools/write_file.rb +5 -0
- data/lib/kward/transcript_export.rb +2 -0
- data/lib/kward/version.rb +2 -1
- data/lib/kward/workspace.rb +45 -5
- metadata +43 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
require "base64"
|
|
2
2
|
require_relative "image_attachments"
|
|
3
3
|
require_relative "message_access"
|
|
4
|
+
require_relative "message_text"
|
|
4
5
|
|
|
6
|
+
# Namespace for the Kward CLI agent runtime.
|
|
5
7
|
module Kward
|
|
8
|
+
# Formats conversation messages for terminal transcript display.
|
|
6
9
|
module CLITranscriptFormatter
|
|
7
10
|
module_function
|
|
8
11
|
|
|
@@ -79,13 +82,7 @@ module Kward
|
|
|
79
82
|
end
|
|
80
83
|
|
|
81
84
|
def full_text(message)
|
|
82
|
-
|
|
83
|
-
text = if content.is_a?(Array)
|
|
84
|
-
content.filter_map { |part| MessageAccess.value(part, :text) }.join("\n")
|
|
85
|
-
else
|
|
86
|
-
content.to_s
|
|
87
|
-
end
|
|
88
|
-
text.strip
|
|
85
|
+
MessageText.full_text(message)
|
|
89
86
|
end
|
|
90
87
|
|
|
91
88
|
def content_part_text(part)
|
data/lib/kward/clipboard.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
require_relative "../message_access"
|
|
2
2
|
require_relative "../tools/tool_call"
|
|
3
3
|
|
|
4
|
+
# Namespace for the Kward CLI agent runtime.
|
|
4
5
|
module Kward
|
|
6
|
+
# Conversation compaction settings, planning, and summary generation.
|
|
5
7
|
module Compaction
|
|
8
|
+
# Tracks file operations while preparing compaction summaries.
|
|
6
9
|
class FileOperationTracker
|
|
7
10
|
def call(messages, previous_details: {})
|
|
8
11
|
read_files = Array(path_values(previous_details, "read_files", :read_files))
|
data/lib/kward/compactor.rb
CHANGED
|
@@ -2,15 +2,23 @@ require "json"
|
|
|
2
2
|
require_relative "model/chat_invocation"
|
|
3
3
|
require_relative "compaction/file_operation_tracker"
|
|
4
4
|
require_relative "config_files"
|
|
5
|
+
require_relative "message_access"
|
|
5
6
|
require_relative "prompts"
|
|
6
7
|
require_relative "tools/tool_call"
|
|
7
8
|
|
|
9
|
+
# Namespace for the Kward CLI agent runtime.
|
|
8
10
|
module Kward
|
|
11
|
+
# Conversation compaction settings, planning, and summary generation.
|
|
9
12
|
module Compaction
|
|
13
|
+
# Compaction support object used by conversation summarization.
|
|
10
14
|
class Error < StandardError; end
|
|
15
|
+
# Compaction support object used by conversation summarization.
|
|
11
16
|
class NothingToCompact < Error; end
|
|
17
|
+
# Compaction support object used by conversation summarization.
|
|
12
18
|
class AlreadyCompacted < Error; end
|
|
19
|
+
# Compaction support object used by conversation summarization.
|
|
13
20
|
class Cancelled < Error; end
|
|
21
|
+
# Compaction support object used by conversation summarization.
|
|
14
22
|
class SummarizationFailed < Error; end
|
|
15
23
|
|
|
16
24
|
PreparationResult = Struct.new(
|
|
@@ -28,6 +36,7 @@ module Kward
|
|
|
28
36
|
|
|
29
37
|
Cut = Struct.new(:first_kept_index, :messages_to_summarize, :turn_prefix_messages, :split_turn, :preserved_messages, :preserved_start_index, keyword_init: true)
|
|
30
38
|
|
|
39
|
+
# Interactive settings menu actions mixed into the CLI frontend.
|
|
31
40
|
class Settings
|
|
32
41
|
DEFAULT_ENABLED = true
|
|
33
42
|
DEFAULT_RESERVE_TOKENS = 16_384
|
|
@@ -35,6 +44,7 @@ module Kward
|
|
|
35
44
|
|
|
36
45
|
attr_reader :enabled, :reserve_tokens, :keep_recent_tokens, :context_window
|
|
37
46
|
|
|
47
|
+
# Creates an object for conversation compaction.
|
|
38
48
|
def initialize(enabled: DEFAULT_ENABLED, reserve_tokens: DEFAULT_RESERVE_TOKENS, keep_recent_tokens: DEFAULT_KEEP_RECENT_TOKENS, context_window: nil)
|
|
39
49
|
@enabled = enabled != false
|
|
40
50
|
@reserve_tokens = positive_integer(reserve_tokens, DEFAULT_RESERVE_TOKENS)
|
|
@@ -64,6 +74,7 @@ module Kward
|
|
|
64
74
|
end
|
|
65
75
|
end
|
|
66
76
|
|
|
77
|
+
# Compaction support object used by conversation summarization.
|
|
67
78
|
class TokenEstimator
|
|
68
79
|
def estimate_tokens(text)
|
|
69
80
|
(text.to_s.length / 4.0).ceil
|
|
@@ -169,9 +180,11 @@ module Kward
|
|
|
169
180
|
end
|
|
170
181
|
end
|
|
171
182
|
|
|
183
|
+
# Compaction support object used by conversation summarization.
|
|
172
184
|
class ConversationSerializer
|
|
173
185
|
TOOL_RESULT_LIMIT = 2_000
|
|
174
186
|
|
|
187
|
+
# Creates an object for conversation compaction.
|
|
175
188
|
def initialize(tool_result_summarizer: nil)
|
|
176
189
|
@tool_result_summarizer = tool_result_summarizer
|
|
177
190
|
end
|
|
@@ -288,28 +301,27 @@ module Kward
|
|
|
288
301
|
end
|
|
289
302
|
|
|
290
303
|
def message_role(message)
|
|
291
|
-
|
|
304
|
+
MessageAccess.role(message)
|
|
292
305
|
end
|
|
293
306
|
|
|
294
307
|
def message_content(message)
|
|
295
|
-
|
|
308
|
+
MessageAccess.content(message)
|
|
296
309
|
end
|
|
297
310
|
|
|
298
311
|
def message_summary(message)
|
|
299
|
-
|
|
312
|
+
MessageAccess.summary(message) || message_content(message)
|
|
300
313
|
end
|
|
301
314
|
|
|
302
315
|
def message_name(message)
|
|
303
|
-
message
|
|
316
|
+
MessageAccess.tool_name(message)
|
|
304
317
|
end
|
|
305
318
|
|
|
306
319
|
def message_tool_call_id(message)
|
|
307
|
-
|
|
320
|
+
MessageAccess.tool_call_id(message)
|
|
308
321
|
end
|
|
309
322
|
|
|
310
323
|
def message_tool_calls(message)
|
|
311
|
-
|
|
312
|
-
value.is_a?(Array) ? value : []
|
|
324
|
+
MessageAccess.tool_calls(message)
|
|
313
325
|
end
|
|
314
326
|
|
|
315
327
|
def tool_call_id(tool_call)
|
|
@@ -350,9 +362,11 @@ module Kward
|
|
|
350
362
|
end
|
|
351
363
|
end
|
|
352
364
|
|
|
365
|
+
# Compaction support object used by conversation summarization.
|
|
353
366
|
class CutPointFinder
|
|
354
367
|
VALID_CUT_ROLES = ["user", "assistant", "bash", "custom", "branchSummary"].freeze
|
|
355
368
|
|
|
369
|
+
# Creates an object for conversation compaction.
|
|
356
370
|
def initialize(estimator: TokenEstimator.new)
|
|
357
371
|
@estimator = estimator
|
|
358
372
|
end
|
|
@@ -431,7 +445,9 @@ module Kward
|
|
|
431
445
|
end
|
|
432
446
|
end
|
|
433
447
|
|
|
448
|
+
# Compaction support object used by conversation summarization.
|
|
434
449
|
class Preparation
|
|
450
|
+
# Creates an object for conversation compaction.
|
|
435
451
|
def initialize(conversation:, settings: Settings.new, estimator: TokenEstimator.new, cut_point_finder: CutPointFinder.new(estimator: estimator), file_operation_tracker: FileOperationTracker.new)
|
|
436
452
|
@conversation = conversation
|
|
437
453
|
@settings = settings
|
|
@@ -521,6 +537,7 @@ module Kward
|
|
|
521
537
|
end
|
|
522
538
|
end
|
|
523
539
|
|
|
540
|
+
# Compaction support object used by conversation summarization.
|
|
524
541
|
class PromptBuilder
|
|
525
542
|
SYSTEM_PROMPT = <<~PROMPT.strip.freeze
|
|
526
543
|
You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.
|
|
@@ -673,6 +690,7 @@ module Kward
|
|
|
673
690
|
Be concise. Focus only on what is needed to understand and continue from the kept suffix. Preserve exact file paths, commands, class names, module names, method names, constants, spec names, migration names, and error messages.
|
|
674
691
|
PROMPT
|
|
675
692
|
|
|
693
|
+
# Creates an object for conversation compaction.
|
|
676
694
|
def initialize(serializer: ConversationSerializer.new)
|
|
677
695
|
@serializer = serializer
|
|
678
696
|
end
|
|
@@ -728,7 +746,9 @@ module Kward
|
|
|
728
746
|
end
|
|
729
747
|
end
|
|
730
748
|
|
|
749
|
+
# Compaction support object used by conversation summarization.
|
|
731
750
|
class Summarizer
|
|
751
|
+
# Creates an object for conversation compaction.
|
|
732
752
|
def initialize(client:, prompt_builder: PromptBuilder.new)
|
|
733
753
|
@client = client
|
|
734
754
|
@prompt_builder = prompt_builder
|
|
@@ -785,6 +805,7 @@ module Kward
|
|
|
785
805
|
end
|
|
786
806
|
end
|
|
787
807
|
|
|
808
|
+
# Compaction support object used by conversation summarization.
|
|
788
809
|
class Compactor
|
|
789
810
|
Result = Struct.new(:summary, :old_message_count, :new_message_count, :first_kept_entry_id, :tokens_before, :details, keyword_init: true)
|
|
790
811
|
NothingToCompact = Compaction::NothingToCompact
|
|
@@ -795,6 +816,7 @@ module Kward
|
|
|
795
816
|
AUTO_COMPACTION_GUARD_RATIO = 0.10
|
|
796
817
|
AUTO_COMPACTION_EXTRA_GUARD_CAP = 12_000
|
|
797
818
|
|
|
819
|
+
# Creates an object for conversation compaction.
|
|
798
820
|
def initialize(conversation:, client:, tool_result_summarizer: nil, settings: nil, summarizer: nil)
|
|
799
821
|
@conversation = conversation
|
|
800
822
|
@client = client
|
data/lib/kward/config_files.rb
CHANGED
|
@@ -5,9 +5,19 @@ require_relative "private_file"
|
|
|
5
5
|
require_relative "prompts/templates"
|
|
6
6
|
require_relative "skills/registry"
|
|
7
7
|
|
|
8
|
+
# Namespace for the Kward CLI agent runtime.
|
|
8
9
|
module Kward
|
|
9
10
|
# Resolves Kward configuration, cache, memory, prompt, skill, and plugin
|
|
10
11
|
# paths, and reads/writes the JSON config file used by the CLI and RPC server.
|
|
12
|
+
#
|
|
13
|
+
# This module is the configuration boundary, not a runtime settings cache.
|
|
14
|
+
# Most methods read the filesystem each time so CLI commands and RPC reloads can
|
|
15
|
+
# observe edits made outside the process. Callers that need caching should own
|
|
16
|
+
# invalidation explicitly, as `Client#reload_config` does for provider state.
|
|
17
|
+
#
|
|
18
|
+
# Keep path decisions here. Higher-level code should ask `ConfigFiles` for
|
|
19
|
+
# config, prompt, skill, plugin, cache, memory, and session locations instead of
|
|
20
|
+
# reconstructing `~/.kward` paths independently.
|
|
11
21
|
module ConfigFiles
|
|
12
22
|
MAX_SKILL_FILE_BYTES = 100_000
|
|
13
23
|
MAX_PROMPT_FILE_BYTES = 32 * 1024
|
|
@@ -73,6 +83,7 @@ module Kward
|
|
|
73
83
|
}
|
|
74
84
|
end
|
|
75
85
|
|
|
86
|
+
# Performs ensure default config for configuration file and path handling.
|
|
76
87
|
def ensure_default_config!(path = config_path)
|
|
77
88
|
path = File.expand_path(path)
|
|
78
89
|
return false if File.exist?(path)
|
|
@@ -105,7 +116,9 @@ module Kward
|
|
|
105
116
|
# Reads the JSON config file.
|
|
106
117
|
#
|
|
107
118
|
# Missing files are treated as an empty config. Invalid JSON raises a
|
|
108
|
-
# user-facing error that includes the file path.
|
|
119
|
+
# user-facing error that includes the file path. This method does not merge
|
|
120
|
+
# defaults; callers should apply feature-specific defaults at the point where
|
|
121
|
+
# behavior is decided.
|
|
109
122
|
#
|
|
110
123
|
# @param path [String] config file path
|
|
111
124
|
# @return [Hash] parsed config object
|
|
@@ -126,6 +139,7 @@ module Kward
|
|
|
126
139
|
PrivateFile.write_json(path, config)
|
|
127
140
|
end
|
|
128
141
|
|
|
142
|
+
# Merges top-level config values and writes the updated config privately.
|
|
129
143
|
def update_config(values, path = config_path)
|
|
130
144
|
raise "Config values must be an object" unless values.is_a?(Hash)
|
|
131
145
|
|
|
@@ -135,6 +149,7 @@ module Kward
|
|
|
135
149
|
config
|
|
136
150
|
end
|
|
137
151
|
|
|
152
|
+
# Removes a top-level config key when it exists.
|
|
138
153
|
def delete_config_key(key, path = config_path)
|
|
139
154
|
config = read_config(path)
|
|
140
155
|
existed = config.key?(key.to_s)
|
|
@@ -143,6 +158,7 @@ module Kward
|
|
|
143
158
|
existed
|
|
144
159
|
end
|
|
145
160
|
|
|
161
|
+
# Returns the first present non-empty string value among several config keys.
|
|
146
162
|
def config_value(config, *keys)
|
|
147
163
|
keys.each do |key|
|
|
148
164
|
text = presence(config[key])
|
|
@@ -166,26 +182,37 @@ module Kward
|
|
|
166
182
|
settings
|
|
167
183
|
end
|
|
168
184
|
|
|
185
|
+
# Returns whether the composer should show busy-state keyboard help.
|
|
169
186
|
def composer_busy_help?(config = read_config)
|
|
170
187
|
composer = config["composer"].is_a?(Hash) ? config["composer"] : {}
|
|
171
188
|
composer["busy_help"] != false
|
|
172
189
|
end
|
|
173
190
|
|
|
191
|
+
# Returns whether the terminal startup banner should be displayed.
|
|
174
192
|
def banner_enabled?(config = read_config)
|
|
175
193
|
banner = config["banner"].is_a?(Hash) ? config["banner"] : {}
|
|
176
194
|
banner["enabled"] != false
|
|
177
195
|
end
|
|
178
196
|
|
|
197
|
+
# Returns whether file tools must stay inside the active workspace root.
|
|
179
198
|
def workspace_guardrails_enabled?(config = read_config)
|
|
180
199
|
tools = config["tools"].is_a?(Hash) ? config["tools"] : {}
|
|
181
200
|
tools["workspace_guardrails"] != false
|
|
182
201
|
end
|
|
183
202
|
|
|
203
|
+
# Returns whether new frontends should resume the last active session automatically.
|
|
184
204
|
def session_auto_resume_enabled?(config = read_config)
|
|
185
205
|
sessions = config["sessions"].is_a?(Hash) ? config["sessions"] : {}
|
|
186
206
|
sessions["auto_resume"] == true
|
|
187
207
|
end
|
|
188
208
|
|
|
209
|
+
# Returns the nested web-search config object, or an empty config when absent.
|
|
210
|
+
def web_search_config(config = read_config)
|
|
211
|
+
value = config["web_search"]
|
|
212
|
+
value.is_a?(Hash) ? value : {}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Validates and persists terminal overlay settings.
|
|
189
216
|
def update_overlay_settings(values)
|
|
190
217
|
raise "Overlay settings must be an object" unless values.is_a?(Hash)
|
|
191
218
|
|
|
@@ -220,6 +247,10 @@ module Kward
|
|
|
220
247
|
# Builds persona prompt text from default, workspace, model, reasoning,
|
|
221
248
|
# time-of-day, weekday, and suffix config entries.
|
|
222
249
|
#
|
|
250
|
+
# Persona resolution is intentionally data-driven so users can edit config
|
|
251
|
+
# without plugin code. Keep new persona selectors additive and deterministic;
|
|
252
|
+
# prompt construction depends on stable ordering.
|
|
253
|
+
#
|
|
223
254
|
# @param workspace_root [String] active workspace root
|
|
224
255
|
# @param model [String, nil] active model name
|
|
225
256
|
# @param reasoning_effort [String, nil] active reasoning effort
|
|
@@ -235,6 +266,7 @@ module Kward
|
|
|
235
266
|
text
|
|
236
267
|
end
|
|
237
268
|
|
|
269
|
+
# Returns the label of the persona selected by default/workspace/model rules.
|
|
238
270
|
def active_persona_label(workspace_root:, model: nil, config: read_config)
|
|
239
271
|
personas = config["personas"]
|
|
240
272
|
return nil unless personas.is_a?(Hash)
|
|
@@ -325,17 +357,6 @@ module Kward
|
|
|
325
357
|
nil
|
|
326
358
|
end
|
|
327
359
|
|
|
328
|
-
def workspace_config(workspace_root, config = read_config)
|
|
329
|
-
workspaces = config["workspaces"]
|
|
330
|
-
return nil unless workspaces.is_a?(Hash)
|
|
331
|
-
|
|
332
|
-
root = canonical_workspace_root(workspace_root)
|
|
333
|
-
workspaces.each do |path, entry|
|
|
334
|
-
return entry if canonical_workspace_root(path) == root
|
|
335
|
-
end
|
|
336
|
-
nil
|
|
337
|
-
end
|
|
338
|
-
|
|
339
360
|
def canonical_workspace_root(path)
|
|
340
361
|
expanded = File.expand_path(path.to_s.empty? ? Dir.pwd : path.to_s)
|
|
341
362
|
File.directory?(expanded) ? File.realpath(expanded) : expanded
|
|
@@ -460,7 +481,6 @@ module Kward
|
|
|
460
481
|
# @return [Array<String>] sorted plugin file paths
|
|
461
482
|
def plugin_paths
|
|
462
483
|
plugins_root = plugin_dir
|
|
463
|
-
warn_legacy_plugin_dir(plugins_root)
|
|
464
484
|
return [] unless Dir.exist?(plugins_root)
|
|
465
485
|
|
|
466
486
|
Dir.glob(File.join(plugins_root, "*.rb")).sort
|
|
@@ -469,17 +489,6 @@ module Kward
|
|
|
469
489
|
[]
|
|
470
490
|
end
|
|
471
491
|
|
|
472
|
-
def warn_legacy_plugin_dir(plugins_root)
|
|
473
|
-
config_path = ENV["KWARD_CONFIG_PATH"]
|
|
474
|
-
return if config_path.to_s.empty?
|
|
475
|
-
|
|
476
|
-
legacy_root = File.expand_path(File.join(File.dirname(config_path), "plugins"))
|
|
477
|
-
return if legacy_root == File.expand_path(plugins_root)
|
|
478
|
-
return unless Dir.exist?(legacy_root)
|
|
479
|
-
|
|
480
|
-
warn "Warning: ignoring Kward plugins in #{legacy_root}; plugins are only loaded from #{File.expand_path(plugins_root)}"
|
|
481
|
-
end
|
|
482
|
-
|
|
483
492
|
# Lists prompt templates exposed as slash commands.
|
|
484
493
|
#
|
|
485
494
|
# @param reserved_commands [Array<String>] command names unavailable to templates
|
data/lib/kward/conversation.rb
CHANGED
|
@@ -4,15 +4,57 @@ require_relative "message_access"
|
|
|
4
4
|
require_relative "plugin_registry"
|
|
5
5
|
require_relative "prompts"
|
|
6
6
|
|
|
7
|
+
# Namespace for the Kward CLI agent runtime.
|
|
7
8
|
module Kward
|
|
9
|
+
# Mutable transcript and runtime context for one agent session.
|
|
10
|
+
#
|
|
11
|
+
# `Conversation` owns message ordering, system prompt refresh, read-before-write
|
|
12
|
+
# state, memory prompt context, and persistence hooks. It intentionally stores
|
|
13
|
+
# plain hashes because provider payload builders, session JSONL files, and RPC
|
|
14
|
+
# normalizers all share the same transcript shape. Use `MessageAccess` when
|
|
15
|
+
# reading messages so symbol/string key and legacy field compatibility stays in
|
|
16
|
+
# one place.
|
|
17
|
+
#
|
|
18
|
+
# Frontends should not mutate `messages` directly after attaching a
|
|
19
|
+
# `SessionStore::Session`; use append/compact helpers so persistence callbacks
|
|
20
|
+
# run and session trees stay consistent.
|
|
8
21
|
class Conversation
|
|
9
22
|
DEFAULT_SYSTEM_MESSAGE = Object.new.freeze
|
|
10
23
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
# @return [Array<Hash>] ordered transcript entries sent to providers and persisted in sessions
|
|
25
|
+
attr_reader :messages
|
|
26
|
+
# @return [Set<String>] resolved paths read by file tools during the active context
|
|
27
|
+
attr_reader :read_paths
|
|
28
|
+
# @return [String] canonical workspace root used for prompts and file guardrails
|
|
29
|
+
attr_reader :workspace_root
|
|
30
|
+
# @return [Hash, nil] system prompt used when summarizing old context
|
|
31
|
+
attr_reader :compaction_system_message
|
|
32
|
+
# @return [String, nil] provider captured for session/runtime prompts
|
|
33
|
+
attr_reader :provider
|
|
34
|
+
# @return [String, nil] model id captured for session/runtime prompts
|
|
35
|
+
attr_reader :model
|
|
36
|
+
# @return [String, nil] reasoning effort captured for session/runtime prompts
|
|
37
|
+
attr_reader :reasoning_effort
|
|
38
|
+
# @return [Array<Hash>] memories scoped to this conversation session
|
|
39
|
+
attr_reader :session_memories
|
|
40
|
+
# @return [Proc, nil] persistence callback invoked after appending a message
|
|
41
|
+
attr_accessor :on_append
|
|
42
|
+
# @return [Proc, nil] persistence callback invoked after compaction replaces history
|
|
43
|
+
attr_accessor :on_compact
|
|
44
|
+
# @return [Proc, nil] callback invoked when a tool execution record should be persisted
|
|
45
|
+
attr_accessor :on_tool_execution
|
|
46
|
+
# @return [Proc, nil] callback invoked when runtime metadata should be persisted
|
|
47
|
+
attr_accessor :on_runtime_update
|
|
48
|
+
# @return [String, nil] memory prompt context injected into refreshed system messages
|
|
49
|
+
attr_accessor :memory_context
|
|
50
|
+
# @return [Hash, nil] metadata for the last memory retrieval attached to the session
|
|
51
|
+
attr_accessor :last_memory_retrieval
|
|
52
|
+
# @return [PluginRegistry, nil] registry used to collect plugin prompt context
|
|
53
|
+
attr_accessor :plugin_registry
|
|
54
|
+
|
|
55
|
+
def initialize(system_message: DEFAULT_SYSTEM_MESSAGE, messages: [], read_paths: [], on_append: nil, on_compact: nil, on_tool_execution: nil, on_runtime_update: nil, workspace_root: Dir.pwd, compaction_system_message: DEFAULT_SYSTEM_MESSAGE, provider: nil, model: nil, reasoning_effort: nil, memory_context: nil, session_memories: [], last_memory_retrieval: nil, plugin_registry: nil)
|
|
15
56
|
@workspace_root = ConfigFiles.canonical_workspace_root(workspace_root)
|
|
57
|
+
@provider = provider
|
|
16
58
|
@model = model
|
|
17
59
|
@reasoning_effort = reasoning_effort
|
|
18
60
|
@plugin_registry = plugin_registry
|
|
@@ -36,8 +78,13 @@ module Kward
|
|
|
36
78
|
@on_append = on_append
|
|
37
79
|
@on_compact = on_compact
|
|
38
80
|
@on_tool_execution = on_tool_execution
|
|
81
|
+
@on_runtime_update = on_runtime_update
|
|
39
82
|
end
|
|
40
83
|
|
|
84
|
+
# Appends a user message and normalizes image attachment syntax.
|
|
85
|
+
#
|
|
86
|
+
# `display_content` is transcript/UI text for cases where the model input is
|
|
87
|
+
# expanded, decorated, or contains encoded attachment content.
|
|
41
88
|
def append_user(content, display_content: nil)
|
|
42
89
|
content = ImageAttachments.content_from_text(content) unless content.is_a?(Array)
|
|
43
90
|
message = { role: "user", content: content }
|
|
@@ -63,6 +110,12 @@ module Kward
|
|
|
63
110
|
@on_tool_execution&.call(tool_call, content)
|
|
64
111
|
end
|
|
65
112
|
|
|
113
|
+
# Rebuilds the system message from current config, memory, plugins, and
|
|
114
|
+
# workspace AGENTS.md state.
|
|
115
|
+
#
|
|
116
|
+
# Conversations created with `system_message: nil` keep system prompts
|
|
117
|
+
# disabled; this preserves tests, compaction summaries, and imported
|
|
118
|
+
# transcripts that intentionally do not include runtime instructions.
|
|
66
119
|
def refresh_system_message!
|
|
67
120
|
return nil unless @system_message_enabled
|
|
68
121
|
|
|
@@ -74,12 +127,17 @@ module Kward
|
|
|
74
127
|
replacement
|
|
75
128
|
end
|
|
76
129
|
|
|
77
|
-
def update_runtime_context!(model:, reasoning_effort:)
|
|
130
|
+
def update_runtime_context!(provider: nil, model:, reasoning_effort:)
|
|
131
|
+
@provider = provider unless provider.to_s.empty?
|
|
78
132
|
@model = model
|
|
79
133
|
@reasoning_effort = reasoning_effort
|
|
80
134
|
refresh_system_message!
|
|
81
135
|
end
|
|
82
136
|
|
|
137
|
+
def persist_runtime_context!
|
|
138
|
+
@on_runtime_update&.call(provider: @provider, model: @model, reasoning_effort: @reasoning_effort)
|
|
139
|
+
end
|
|
140
|
+
|
|
83
141
|
def refresh_system_message_if_workspace_agents_changed!
|
|
84
142
|
refresh_system_message! if @system_message_enabled && workspace_agents_mtime != @workspace_agents_mtime
|
|
85
143
|
end
|
|
@@ -95,6 +153,13 @@ module Kward
|
|
|
95
153
|
plugin_registry.prompt_context(context)
|
|
96
154
|
end
|
|
97
155
|
|
|
156
|
+
# Replaces most transcript entries with a compaction summary and optional
|
|
157
|
+
# recent messages to keep.
|
|
158
|
+
#
|
|
159
|
+
# Compaction clears read-before-write state because file contents observed
|
|
160
|
+
# before the summary may no longer be represented exactly in the active
|
|
161
|
+
# context. Callers that need file mutation after compaction should read files
|
|
162
|
+
# again through the normal tools.
|
|
98
163
|
def compact!(summary, compaction_summary: false, first_kept_entry_id: nil, tokens_before: nil, from_hook: false, details: {}, keep_messages: [])
|
|
99
164
|
message = if compaction_summary
|
|
100
165
|
{ role: "compactionSummary", summary: summary.to_s }
|
data/lib/kward/events.rb
CHANGED
data/lib/kward/export_path.rb
CHANGED
|
@@ -4,7 +4,9 @@ require "shellwords"
|
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require "uri"
|
|
6
6
|
|
|
7
|
+
# Namespace for the Kward CLI agent runtime.
|
|
7
8
|
module Kward
|
|
9
|
+
# Image attachment parsing, validation, encoding, and display helpers.
|
|
8
10
|
module ImageAttachments
|
|
9
11
|
MAX_IMAGE_BYTES = 20 * 1024 * 1024
|
|
10
12
|
MIME_TYPES = {
|
data/lib/kward/memory/manager.rb
CHANGED
|
@@ -7,7 +7,9 @@ require_relative "../config_files"
|
|
|
7
7
|
require_relative "../message_access"
|
|
8
8
|
require_relative "../model/client"
|
|
9
9
|
|
|
10
|
+
# Namespace for the Kward CLI agent runtime.
|
|
10
11
|
module Kward
|
|
12
|
+
# Memory subsystem for core, soft, session, and retrieval state.
|
|
11
13
|
module Memory
|
|
12
14
|
# Manages Kward's opt-in structured memory store.
|
|
13
15
|
#
|
|
@@ -35,6 +37,7 @@ module Kward
|
|
|
35
37
|
)
|
|
36
38
|
end
|
|
37
39
|
|
|
40
|
+
# Creates an object for memory storage and retrieval.
|
|
38
41
|
def initialize(config_path: ConfigFiles.config_path, core_path: ConfigFiles.memory_core_path, soft_path: ConfigFiles.memory_soft_path, events_path: ConfigFiles.memory_events_path, now: nil)
|
|
39
42
|
@config_path = config_path
|
|
40
43
|
@core_path = core_path
|
|
@@ -299,6 +302,11 @@ module Kward
|
|
|
299
302
|
@last_retrieval || { "enabled" => enabled?, "core" => [], "soft" => [], "reasons" => [], "message" => "No memory retrieval has run yet." }
|
|
300
303
|
end
|
|
301
304
|
|
|
305
|
+
# Formats retrieved memories for system prompt injection.
|
|
306
|
+
#
|
|
307
|
+
# Keep this block compact and explicit: it is read by the model, shown in
|
|
308
|
+
# transcripts, and explained by `/memory why`. Do not include inactive or
|
|
309
|
+
# forgotten memories here; retrieval already decides the active set.
|
|
302
310
|
def memory_block(retrieval)
|
|
303
311
|
core = Array(retrieval["core"])
|
|
304
312
|
soft = Array(retrieval["soft"])
|
|
@@ -330,6 +338,11 @@ module Kward
|
|
|
330
338
|
lines.join("\n")
|
|
331
339
|
end
|
|
332
340
|
|
|
341
|
+
# Infers bounded session/workspace soft memories from a conversation.
|
|
342
|
+
#
|
|
343
|
+
# This is best-effort and intentionally conservative. It may use an LLM when
|
|
344
|
+
# configured, but failures fall back to heuristic text or no-op behavior so
|
|
345
|
+
# memory summarization never blocks normal session flow.
|
|
333
346
|
def summarize_conversation(conversation, client: nil)
|
|
334
347
|
text = messages_for_summarization(conversation).map { |message| MessageAccess.content(message) }.compact.join("\n")
|
|
335
348
|
existing_texts = Array(conversation.session_memories).map { |memory| memory["text"] }
|
data/lib/kward/message_access.rb
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
1
2
|
module Kward
|
|
3
|
+
# Compatibility reader for persisted conversation message hashes.
|
|
4
|
+
#
|
|
5
|
+
# Kward stores transcript entries as plain hashes because model payloads,
|
|
6
|
+
# JSONL sessions, plugins, and RPC normalizers all need to pass them around
|
|
7
|
+
# without framework objects. Restored sessions may contain either symbol keys,
|
|
8
|
+
# string keys, or Tauren-style camelCase aliases. `MessageAccess` centralizes
|
|
9
|
+
# those lookup rules so callers do not grow one-off compatibility branches.
|
|
2
10
|
module MessageAccess
|
|
3
11
|
module_function
|
|
4
12
|
|
|
13
|
+
# Reads a field from a hash-like object using symbol or string keys.
|
|
14
|
+
#
|
|
15
|
+
# @param object [#key?, nil] hash-like object to read
|
|
16
|
+
# @param key [String, Symbol] canonical field name
|
|
17
|
+
# @return [Object, nil] stored value when present
|
|
5
18
|
def value(object, key)
|
|
6
19
|
return nil unless object.respond_to?(:key?)
|
|
7
20
|
return object[key] if object.key?(key)
|
|
@@ -10,14 +23,17 @@ module Kward
|
|
|
10
23
|
nil
|
|
11
24
|
end
|
|
12
25
|
|
|
26
|
+
# @return [String, nil] message role such as `user`, `assistant`, or `tool`
|
|
13
27
|
def role(message)
|
|
14
28
|
value(message, :role)
|
|
15
29
|
end
|
|
16
30
|
|
|
31
|
+
# @return [Object, nil] raw message content
|
|
17
32
|
def content(message)
|
|
18
33
|
value(message, :content)
|
|
19
34
|
end
|
|
20
35
|
|
|
36
|
+
# @return [String, nil] UI-facing content preserved separately from model input
|
|
21
37
|
def display_content(message)
|
|
22
38
|
value(message, :display_content) || value(message, :displayContent)
|
|
23
39
|
end
|
|
@@ -31,11 +47,16 @@ module Kward
|
|
|
31
47
|
end
|
|
32
48
|
|
|
33
49
|
def tool_call_id(message)
|
|
34
|
-
value(message, :tool_call_id)
|
|
50
|
+
value(message, :tool_call_id) || value(message, :toolCallId)
|
|
35
51
|
end
|
|
36
52
|
|
|
53
|
+
def tool_name(message)
|
|
54
|
+
value(message, :name) || value(message, :toolName)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Array<Hash>] assistant tool calls, or an empty array
|
|
37
58
|
def tool_calls(message)
|
|
38
|
-
calls = value(message, :tool_calls)
|
|
59
|
+
calls = value(message, :tool_calls) || value(message, :toolCalls)
|
|
39
60
|
calls.is_a?(Array) ? calls : []
|
|
40
61
|
end
|
|
41
62
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require_relative "message_access"
|
|
2
|
+
|
|
3
|
+
# Namespace for the Kward CLI agent runtime.
|
|
4
|
+
module Kward
|
|
5
|
+
# Builds user-visible plain text from persisted conversation messages.
|
|
6
|
+
#
|
|
7
|
+
# Conversations may store one value for the model and another value for the UI.
|
|
8
|
+
# Prompt templates, for example, keep expanded instructions in `content` while
|
|
9
|
+
# preserving the submitted slash command in `display_content`. This helper keeps
|
|
10
|
+
# tree navigation, forks, copy/export features, and RPC payloads aligned on the
|
|
11
|
+
# same visible text rules.
|
|
12
|
+
module MessageText
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Returns the plain text a user should see or edit for a message.
|
|
16
|
+
#
|
|
17
|
+
# User messages prefer `display_content`/`displayContent` when present. Other
|
|
18
|
+
# messages, and user messages without display text, are reduced from their
|
|
19
|
+
# stored content. Array content contributes only textual parts so image and
|
|
20
|
+
# tool-call blocks do not leak implementation details into editable text.
|
|
21
|
+
#
|
|
22
|
+
# @param message [Hash] persisted conversation message
|
|
23
|
+
# @return [String] stripped visible text
|
|
24
|
+
def full_text(message)
|
|
25
|
+
display_content = MessageAccess.display_content(message)
|
|
26
|
+
return display_content.to_s.strip unless display_content.nil?
|
|
27
|
+
|
|
28
|
+
content_text(MessageAccess.content(message)).strip
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Converts message content into plain text without applying display-content
|
|
32
|
+
# overrides.
|
|
33
|
+
#
|
|
34
|
+
# @param content [String, Array<Hash>, nil] message content field
|
|
35
|
+
# @return [String] textual content joined with newlines
|
|
36
|
+
def content_text(content)
|
|
37
|
+
case content
|
|
38
|
+
when Array
|
|
39
|
+
content.filter_map { |part| MessageAccess.value(part, :text) }.join("\n")
|
|
40
|
+
else
|
|
41
|
+
content.to_s
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|