kward 0.84.0 → 0.85.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 +34 -0
- data/Gemfile.lock +2 -2
- data/README.md +2 -2
- data/doc/agent-tools.md +13 -1
- data/doc/api.md +17 -2
- data/doc/composer.md +1 -1
- data/doc/configuration.md +27 -4
- data/doc/extensibility.md +2 -1
- data/doc/git.md +3 -1
- data/doc/pan.md +6 -0
- data/doc/permissions.md +4 -4
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +7 -1
- data/doc/session-management.md +5 -4
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/usage.md +4 -1
- data/doc/workspace-tools.md +9 -0
- data/examples/plugins/space_invaders.rb +1 -1
- data/examples/plugins/stardate_footer.rb +2 -2
- data/examples/plugins/telegram/plugin.rb +1 -1
- data/lib/kward/agent.rb +24 -11
- data/lib/kward/cli/compaction.rb +9 -3
- data/lib/kward/cli/plugins.rb +81 -12
- data/lib/kward/cli/prompt_interface.rb +25 -5
- data/lib/kward/cli/rendering.rb +3 -0
- data/lib/kward/cli/runtime_helpers.rb +121 -27
- data/lib/kward/cli/sessions.rb +9 -5
- data/lib/kward/cli/settings/menus.rb +745 -0
- data/lib/kward/cli/settings/model.rb +327 -0
- data/lib/kward/cli/settings.rb +6 -1055
- data/lib/kward/cli/slash_commands.rb +45 -4
- data/lib/kward/cli/tabs.rb +167 -27
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +3 -3
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +26 -24
- data/lib/kward/compactor.rb +18 -7
- data/lib/kward/config/core.rb +389 -0
- data/lib/kward/config/extensions.rb +96 -0
- data/lib/kward/config/prompts.rb +313 -0
- data/lib/kward/config/settings.rb +250 -0
- data/lib/kward/config_files.rb +14 -1008
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +2 -2
- data/lib/kward/model/copilot_models.rb +2 -2
- data/lib/kward/model/model_info.rb +20 -3
- data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
- data/lib/kward/model/payloads.rb +12 -3
- data/lib/kward/model/typesafe_client.rb +78 -0
- data/lib/kward/pan/server.rb +10 -7
- data/lib/kward/permissions/policy.rb +6 -2
- data/lib/kward/plugin_registry.rb +2 -659
- data/lib/kward/plugins/actions.rb +453 -0
- data/lib/kward/plugins/chat_contract.rb +121 -0
- data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
- data/lib/kward/plugins/host.rb +232 -0
- data/lib/kward/plugins/registry.rb +1190 -0
- data/lib/kward/plugins/resources.rb +206 -0
- data/lib/kward/plugins/turn_request.rb +36 -0
- data/lib/kward/plugins/ui.rb +219 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -1
- data/lib/kward/prompt_interface/composer_state.rb +1 -1
- data/lib/kward/prompt_interface/editor/controller.rb +1 -1
- data/lib/kward/prompt_interface/editor/runner.rb +1 -1
- data/lib/kward/prompt_interface/editor/state.rb +1 -1
- data/lib/kward/prompt_interface/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +1 -1
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/runtime_state.rb +6 -1
- data/lib/kward/prompt_interface/screen.rb +9 -2
- data/lib/kward/prompt_interface.rb +60 -11
- data/lib/kward/prompts/commands.rb +2 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +2 -2
- data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
- data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
- data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +9 -4
- data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
- data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
- data/lib/kward/rpc/prompt_bridge.rb +25 -0
- data/lib/kward/rpc/server.rb +91 -12
- data/lib/kward/rpc/session_manager.rb +147 -44
- data/lib/kward/rpc/session_tree_rows.rb +2 -2
- data/lib/kward/rpc/tool_metadata.rb +1 -1
- data/lib/kward/sandbox/command_runner.rb +1 -1
- data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
- data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
- data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
- data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
- data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
- data/lib/kward/{kwsh.rb → shell/kwsh.rb} +3 -3
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +3 -3
- data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
- data/lib/kward/skills/trust_store.rb +1 -1
- data/lib/kward/tabs/driver.rb +194 -0
- data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
- data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
- data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
- data/lib/kward/tools/base.rb +18 -0
- data/lib/kward/tools/context_for_task.rb +15 -6
- data/lib/kward/tools/edit_file.rb +9 -6
- data/lib/kward/tools/git_commit.rb +13 -7
- data/lib/kward/tools/list_directory.rb +4 -4
- data/lib/kward/tools/plugin_tool.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +1 -1
- data/lib/kward/tools/read_file.rb +7 -6
- data/lib/kward/tools/registry.rb +96 -13
- data/lib/kward/tools/run_shell_command.rb +10 -8
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/summarize_file_structure.rb +5 -5
- data/lib/kward/tools/tool_call.rb +2 -1
- data/lib/kward/tools/typesafe_evaluate.rb +81 -0
- data/lib/kward/tools/workspace_targets.rb +58 -0
- data/lib/kward/tools/write_file.rb +9 -6
- data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
- data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
- data/lib/kward/transport/contracts.rb +200 -0
- data/lib/kward/transport/gateway.rb +79 -35
- data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
- data/lib/kward/transport.rb +1 -200
- data/lib/kward/version.rb +1 -1
- data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
- data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
- data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
- data/templates/default/fulldoc/html/css/kward.css +125 -0
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -1
- data/templates/default/layout/html/layout.erb +16 -4
- metadata +67 -48
- data/lib/kward/tab_driver.rb +0 -90
- data/templates/default/fulldoc/html/images/kward_workflow.svg +0 -52
- /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
- /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
- /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
- /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
- /data/lib/kward/{markdown_code_block.rb → prompt_interface/editor/markdown_code_block.rb} +0 -0
- /data/lib/kward/{scratchpad_languages.rb → prompt_interface/editor/scratchpad_languages.rb} +0 -0
- /data/lib/kward/{scratchpad_runner.rb → prompt_interface/editor/scratchpad_runner.rb} +0 -0
- /data/lib/kward/{detached_run.rb → pty/detached_run.rb} +0 -0
- /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
- /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
- /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
- /data/lib/kward/{kwshrc.rb → shell/kwshrc.rb} +0 -0
- /data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +0 -0
- /data/lib/kward/{ansi.rb → terminal/ansi.rb} +0 -0
- /data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +0 -0
- /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{terminal_text.rb → terminal/text.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{project_files.rb → workspace/files.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
data/lib/kward/tools/registry.rb
CHANGED
|
@@ -11,13 +11,16 @@ require_relative "fetch_raw"
|
|
|
11
11
|
require_relative "git_commit"
|
|
12
12
|
require_relative "list_directory"
|
|
13
13
|
require_relative "mcp_tool"
|
|
14
|
+
require_relative "plugin_tool"
|
|
14
15
|
require_relative "read_file"
|
|
15
16
|
require_relative "read_skill"
|
|
16
17
|
require_relative "run_shell_command"
|
|
17
18
|
require_relative "prepare_shell_command"
|
|
18
19
|
require_relative "summarize_file_structure"
|
|
20
|
+
require_relative "typesafe_evaluate"
|
|
19
21
|
require_relative "retrieve_tool_output"
|
|
20
22
|
require_relative "web_search"
|
|
23
|
+
require_relative "workspace_targets"
|
|
21
24
|
require_relative "write_file"
|
|
22
25
|
require_relative "open_editor"
|
|
23
26
|
require_relative "replace_editor_buffer"
|
|
@@ -29,7 +32,7 @@ require_relative "../mcp/server_config"
|
|
|
29
32
|
require_relative "../permissions/policy"
|
|
30
33
|
require_relative "../telemetry/logger"
|
|
31
34
|
require_relative "../tool_output_compactor"
|
|
32
|
-
require_relative "../workspace"
|
|
35
|
+
require_relative "../workspace/workspace"
|
|
33
36
|
|
|
34
37
|
# Namespace for the Kward CLI agent runtime.
|
|
35
38
|
module Kward
|
|
@@ -66,6 +69,30 @@ module Kward
|
|
|
66
69
|
retrieve_tool_output
|
|
67
70
|
].freeze
|
|
68
71
|
|
|
72
|
+
TARGETED_WORKSPACE_TOOLS = %w[
|
|
73
|
+
list_directory
|
|
74
|
+
read_file
|
|
75
|
+
write_file
|
|
76
|
+
edit_file
|
|
77
|
+
run_shell_command
|
|
78
|
+
summarize_file_structure
|
|
79
|
+
context_for_task
|
|
80
|
+
git_commit
|
|
81
|
+
].freeze
|
|
82
|
+
|
|
83
|
+
BUILTIN_TOOL_NAMES = (CORE_TOOL_NAMES + %w[
|
|
84
|
+
git_commit
|
|
85
|
+
web_search
|
|
86
|
+
fetch_content
|
|
87
|
+
fetch_raw
|
|
88
|
+
read_skill
|
|
89
|
+
ask_user_question
|
|
90
|
+
replace_editor_buffer
|
|
91
|
+
open_editor
|
|
92
|
+
prepare_shell_command
|
|
93
|
+
typesafe_evaluate
|
|
94
|
+
]).freeze
|
|
95
|
+
|
|
69
96
|
# Tool schemas advertised to the model for the current frontend and config.
|
|
70
97
|
#
|
|
71
98
|
# @return [Array<Hash>] tool schemas currently advertised to the model
|
|
@@ -74,6 +101,7 @@ module Kward
|
|
|
74
101
|
# Builds tool objects and the schema list for the current frontend/config.
|
|
75
102
|
#
|
|
76
103
|
# @param workspace [Workspace] filesystem/shell boundary used by local tools
|
|
104
|
+
# @param workspace_targets [Tools::WorkspaceTargets, nil] additional host-scoped worktree roles
|
|
77
105
|
# @param prompt [Object, nil] interactive prompt bridge; tools such as
|
|
78
106
|
# `ask_user_question` and `open_editor` are advertised only when supported
|
|
79
107
|
# @param web_search [WebSearch] live web search implementation
|
|
@@ -82,12 +110,15 @@ module Kward
|
|
|
82
110
|
# @param web_search_enabled [Boolean, nil] override for web search exposure
|
|
83
111
|
# @param skills [Array<ConfigFiles::Skill>, nil] override discovered skills
|
|
84
112
|
# @param ask_user_question_enabled [Boolean, nil] override question exposure
|
|
85
|
-
|
|
113
|
+
# @param plugin_tools [Array<PluginRegistry::Tool>] trusted plugin tool registrations
|
|
114
|
+
def initialize(workspace: Workspace.new, workspace_targets: nil, prompt: nil, web_search: WebSearch.new, web_fetch: WebFetch.new, code_search: CodeSearch.new, web_search_enabled: nil, skills: nil, ask_user_question_enabled: nil, allowed_tool_names: nil, editor_prompt_session: nil, tool_output_compactor: ToolOutputCompactor.new, telemetry_logger: TelemetryLogger.new, context_budget_meter: nil, mcp_clients: nil, plugin_tools: [], typesafe_client: TypeSafeClient.new, tool_approval: nil, approval_for_allowed_tools: false, permission_policy: nil, hook_manager: nil, hook_context: nil, git_committer: nil)
|
|
86
115
|
@workspace = workspace
|
|
116
|
+
@workspace_targets = workspace_targets || Tools::WorkspaceTargets.new(active: workspace)
|
|
87
117
|
@prompt = prompt
|
|
88
118
|
@web_search = web_search
|
|
89
119
|
@web_fetch = web_fetch
|
|
90
120
|
@code_search = code_search
|
|
121
|
+
@typesafe_client = typesafe_client
|
|
91
122
|
@skills = skills
|
|
92
123
|
@web_search_enabled = web_search_enabled
|
|
93
124
|
@ask_user_question_enabled = ask_user_question_enabled
|
|
@@ -97,6 +128,7 @@ module Kward
|
|
|
97
128
|
@tool_output_compactor = tool_output_compactor
|
|
98
129
|
@telemetry_logger = telemetry_logger
|
|
99
130
|
@context_budget_meter = context_budget_meter
|
|
131
|
+
@plugin_tools = Array(plugin_tools)
|
|
100
132
|
@tool_approval = tool_approval
|
|
101
133
|
@approval_for_allowed_tools = approval_for_allowed_tools == true
|
|
102
134
|
@permission_policy = permission_policy || Permissions::Policy.from_config(ConfigFiles.read_config)
|
|
@@ -114,6 +146,16 @@ module Kward
|
|
|
114
146
|
@schemas = build_schema_tools.map { |tool| schema_with_metadata(tool) }.freeze
|
|
115
147
|
end
|
|
116
148
|
|
|
149
|
+
# Replaces plugin registrations while preserving this registry's frontend,
|
|
150
|
+
# workspace, permission, hook, and execution-profile restrictions.
|
|
151
|
+
def replace_plugin_tools!(plugin_tools)
|
|
152
|
+
@plugin_tools = Array(plugin_tools)
|
|
153
|
+
@plugin_tool_values = nil
|
|
154
|
+
@tools = build_tools.freeze
|
|
155
|
+
@schemas = build_schema_tools.map { |tool| schema_with_metadata(tool) }.freeze
|
|
156
|
+
self
|
|
157
|
+
end
|
|
158
|
+
|
|
117
159
|
# Builds a registry scoped to one editor prompt turn.
|
|
118
160
|
#
|
|
119
161
|
# Editor prompts may inspect the workspace, but their only write operation
|
|
@@ -163,7 +205,10 @@ module Kward
|
|
|
163
205
|
original_content = if tool
|
|
164
206
|
before_tool = run_hook("tool_call_before", conversation, payload: tool_payload(name, args, tool_call))
|
|
165
207
|
args = hook_arguments(before_tool, args)
|
|
166
|
-
|
|
208
|
+
target_error = workspace_target_error(name, args)
|
|
209
|
+
if target_error
|
|
210
|
+
target_error
|
|
211
|
+
elsif before_tool.denied?
|
|
167
212
|
hook_denied_content(before_tool, "tool call denied: #{name}")
|
|
168
213
|
elsif before_tool.approval_required? && hook_approval_denied?(before_tool, tool_call, name, args, cancellation)
|
|
169
214
|
hook_denied_content(before_tool, "tool call approval denied: #{name}")
|
|
@@ -254,6 +299,8 @@ module Kward
|
|
|
254
299
|
case tool
|
|
255
300
|
when Tools::MCPTool
|
|
256
301
|
"mcp"
|
|
302
|
+
when Tools::PluginTool
|
|
303
|
+
"plugin"
|
|
257
304
|
when Tools::WebSearch, Tools::FetchContent, Tools::FetchRaw
|
|
258
305
|
"web"
|
|
259
306
|
when Tools::ReadSkill
|
|
@@ -491,6 +538,13 @@ module Kward
|
|
|
491
538
|
(metadata[:source] || metadata["source"]).to_s == "mcp"
|
|
492
539
|
end
|
|
493
540
|
|
|
541
|
+
def workspace_target_error(name, args)
|
|
542
|
+
return nil unless TARGETED_WORKSPACE_TOOLS.include?(name.to_s)
|
|
543
|
+
return nil if @workspace_targets.valid?(args)
|
|
544
|
+
|
|
545
|
+
"Error: Unknown workspace target: #{@workspace_targets.target_name(args)}"
|
|
546
|
+
end
|
|
547
|
+
|
|
494
548
|
def permission_approval_result(tool_call, name, args, cancellation)
|
|
495
549
|
return false unless @tool_approval
|
|
496
550
|
|
|
@@ -514,6 +568,11 @@ module Kward
|
|
|
514
568
|
tools = all_tools
|
|
515
569
|
tools = tools.select { |tool| @allowed_tool_names.include?(tool.name) } if @allowed_tool_names
|
|
516
570
|
tools.each_with_object({}) do |tool, result|
|
|
571
|
+
if tool.is_a?(Tools::PluginTool) && (BUILTIN_TOOL_NAMES.include?(tool.name) || result.key?(tool.name))
|
|
572
|
+
source = tool.plugin_path.to_s.empty? ? "" : " (#{tool.plugin_path})"
|
|
573
|
+
ConfigFiles.emit_warning("Warning: skipping Kward plugin tool #{tool.name}#{source}: duplicate tool name")
|
|
574
|
+
next
|
|
575
|
+
end
|
|
517
576
|
raise ArgumentError, "Duplicate tool name: #{tool.name}" if result.key?(tool.name)
|
|
518
577
|
|
|
519
578
|
result[tool.name] = tool
|
|
@@ -528,6 +587,8 @@ module Kward
|
|
|
528
587
|
tools << @tools["git_commit"] if @tools["git_commit"]
|
|
529
588
|
tools.concat(@tools.values_at("web_search", "fetch_content", "fetch_raw")) if web_search_available?
|
|
530
589
|
tools.concat(@tools.values.select { |tool| tool.is_a?(Tools::MCPTool) })
|
|
590
|
+
tools.concat(@tools.values.select { |tool| tool.is_a?(Tools::PluginTool) })
|
|
591
|
+
tools << @tools["typesafe_evaluate"] if @tools["typesafe_evaluate"]
|
|
531
592
|
tools << @tools["read_skill"] if skills_available?
|
|
532
593
|
tools << @tools["ask_user_question"] if ask_user_question_available?
|
|
533
594
|
tools.compact
|
|
@@ -535,26 +596,27 @@ module Kward
|
|
|
535
596
|
|
|
536
597
|
def all_tools
|
|
537
598
|
tools = core_tools
|
|
538
|
-
tools << Tools::GitCommit.new(committer: @git_committer) if @git_committer
|
|
539
|
-
tools + [
|
|
599
|
+
tools << Tools::GitCommit.new(committer: @git_committer, workspace_targets: @workspace_targets) if @git_committer
|
|
600
|
+
(tools + [
|
|
601
|
+
(Tools::TypeSafeEvaluate.new(client: @typesafe_client) if @typesafe_client&.available?),
|
|
540
602
|
Tools::WebSearch.new(web_search: @web_search),
|
|
541
603
|
Tools::FetchContent.new(web_fetch: @web_fetch),
|
|
542
604
|
Tools::FetchRaw.new(web_fetch: @web_fetch),
|
|
543
605
|
Tools::ReadSkill.new(skills: discovered_skills),
|
|
544
606
|
Tools::AskUserQuestion.new(prompt: @prompt)
|
|
545
|
-
] + mcp_tool_values
|
|
607
|
+
]).compact + mcp_tool_values + plugin_tool_values
|
|
546
608
|
end
|
|
547
609
|
|
|
548
610
|
def core_tools
|
|
549
611
|
tools = [
|
|
550
|
-
Tools::ListDirectory.new(workspace: @workspace),
|
|
551
|
-
Tools::ReadFile.new(workspace: @workspace),
|
|
552
|
-
Tools::WriteFile.new(workspace: @workspace),
|
|
553
|
-
Tools::EditFile.new(workspace: @workspace),
|
|
612
|
+
Tools::ListDirectory.new(workspace: @workspace, workspace_targets: @workspace_targets),
|
|
613
|
+
Tools::ReadFile.new(workspace: @workspace, workspace_targets: @workspace_targets),
|
|
614
|
+
Tools::WriteFile.new(workspace: @workspace, workspace_targets: @workspace_targets),
|
|
615
|
+
Tools::EditFile.new(workspace: @workspace, workspace_targets: @workspace_targets),
|
|
554
616
|
shell_command_tool,
|
|
555
617
|
Tools::CodeSearch.new(code_search: @code_search),
|
|
556
|
-
Tools::SummarizeFileStructure.new(workspace: @workspace),
|
|
557
|
-
Tools::ContextForTask.new(workspace: @workspace),
|
|
618
|
+
Tools::SummarizeFileStructure.new(workspace: @workspace, workspace_targets: @workspace_targets),
|
|
619
|
+
Tools::ContextForTask.new(workspace: @workspace, workspace_targets: @workspace_targets),
|
|
558
620
|
Tools::ContextBudgetStats.new(context_budget_meter: @context_budget_meter),
|
|
559
621
|
Tools::RetrieveToolOutput.new
|
|
560
622
|
]
|
|
@@ -568,7 +630,7 @@ module Kward
|
|
|
568
630
|
if @shell_prompt_session
|
|
569
631
|
Tools::RunShellCommand.new(shell_prompt_session: @shell_prompt_session)
|
|
570
632
|
else
|
|
571
|
-
Tools::RunShellCommand.new(workspace: @workspace)
|
|
633
|
+
Tools::RunShellCommand.new(workspace: @workspace, workspace_targets: @workspace_targets)
|
|
572
634
|
end
|
|
573
635
|
end
|
|
574
636
|
|
|
@@ -611,6 +673,27 @@ module Kward
|
|
|
611
673
|
@mcp_tool_values ||= build_mcp_tools
|
|
612
674
|
end
|
|
613
675
|
|
|
676
|
+
def plugin_tool_values
|
|
677
|
+
@plugin_tool_values ||= @plugin_tools.map do |registration|
|
|
678
|
+
Tools::PluginTool.new(
|
|
679
|
+
registration: registration,
|
|
680
|
+
context_factory: method(:plugin_tool_context)
|
|
681
|
+
)
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def plugin_tool_context(conversation, cancellation)
|
|
686
|
+
if @hook_context.respond_to?(:for_tool)
|
|
687
|
+
@hook_context.for_tool(conversation: conversation, cancellation: cancellation)
|
|
688
|
+
else
|
|
689
|
+
PluginRegistry::Context.new(
|
|
690
|
+
conversation: conversation,
|
|
691
|
+
workspace_root: @workspace.respond_to?(:root) ? @workspace.root.to_s : Dir.pwd,
|
|
692
|
+
cancellation: cancellation
|
|
693
|
+
)
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
614
697
|
def build_mcp_tools
|
|
615
698
|
Array(@mcp_clients).flat_map do |client|
|
|
616
699
|
client.list_tools.map do |tool|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require_relative "base"
|
|
2
|
-
require_relative "../workspace"
|
|
2
|
+
require_relative "../workspace/workspace"
|
|
3
3
|
|
|
4
4
|
# Namespace for the Kward CLI agent runtime.
|
|
5
5
|
module Kward
|
|
@@ -8,11 +8,11 @@ module Kward
|
|
|
8
8
|
# Tool wrapper for bounded shell commands in the workspace or active shell.
|
|
9
9
|
class RunShellCommand < Base
|
|
10
10
|
# Builds the tool schema and stores the execution dependency.
|
|
11
|
-
def initialize(workspace: nil, shell_prompt_session: nil)
|
|
11
|
+
def initialize(workspace: nil, workspace_targets: nil, shell_prompt_session: nil)
|
|
12
12
|
raise ArgumentError, "RunShellCommand requires a workspace or shell session" unless workspace || shell_prompt_session
|
|
13
13
|
raise ArgumentError, "RunShellCommand accepts only one execution target" if workspace && shell_prompt_session
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
configure_workspace_targets(workspace, workspace_targets) if workspace
|
|
16
16
|
@shell_prompt_session = shell_prompt_session
|
|
17
17
|
description = shell_prompt_session ?
|
|
18
18
|
"Run a bounded noninteractive command in the active embedded shell session." :
|
|
@@ -20,13 +20,15 @@ module Kward
|
|
|
20
20
|
timeout_description = shell_prompt_session ?
|
|
21
21
|
"Timeout seconds; defaults to the embedded shell setting." :
|
|
22
22
|
"Timeout seconds; default 30."
|
|
23
|
+
properties = {
|
|
24
|
+
command: { type: "string", description: "Command to run." },
|
|
25
|
+
timeout_seconds: { type: "integer", description: timeout_description }
|
|
26
|
+
}
|
|
27
|
+
properties = targeted_properties(properties) if workspace
|
|
23
28
|
super(
|
|
24
29
|
"run_shell_command",
|
|
25
30
|
description,
|
|
26
|
-
properties:
|
|
27
|
-
command: { type: "string", description: "Command to run." },
|
|
28
|
-
timeout_seconds: { type: "integer", description: timeout_description }
|
|
29
|
-
},
|
|
31
|
+
properties: properties,
|
|
30
32
|
required: ["command"]
|
|
31
33
|
)
|
|
32
34
|
end
|
|
@@ -43,7 +45,7 @@ module Kward
|
|
|
43
45
|
if @shell_prompt_session
|
|
44
46
|
@shell_prompt_session.run(command, timeout_seconds: timeout_seconds, cancellation: cancellation)
|
|
45
47
|
else
|
|
46
|
-
|
|
48
|
+
workspace_for(args).run_shell_command(command, timeout_seconds: timeout_seconds, cancellation: cancellation)
|
|
47
49
|
end
|
|
48
50
|
end
|
|
49
51
|
end
|
|
@@ -7,14 +7,14 @@ module Kward
|
|
|
7
7
|
# Returns a compact symbol outline for a workspace source file.
|
|
8
8
|
class SummarizeFileStructure < Base
|
|
9
9
|
# Builds the tool schema and stores the execution dependency.
|
|
10
|
-
def initialize(workspace:)
|
|
11
|
-
|
|
10
|
+
def initialize(workspace:, workspace_targets: nil)
|
|
11
|
+
configure_workspace_targets(workspace, workspace_targets)
|
|
12
12
|
super(
|
|
13
13
|
"summarize_file_structure",
|
|
14
14
|
"Return a compact outline of classes, modules, methods, and functions in a workspace source file.",
|
|
15
|
-
properties:
|
|
15
|
+
properties: targeted_properties(
|
|
16
16
|
path: { type: "string", description: "Workspace-relative source file path." }
|
|
17
|
-
|
|
17
|
+
),
|
|
18
18
|
required: ["path"]
|
|
19
19
|
)
|
|
20
20
|
end
|
|
@@ -22,7 +22,7 @@ module Kward
|
|
|
22
22
|
# Executes the structure summary tool.
|
|
23
23
|
def call(args, _conversation, cancellation: nil)
|
|
24
24
|
cancellation&.raise_if_cancelled!
|
|
25
|
-
|
|
25
|
+
workspace_for(args).summarize_file_structure(argument(args, :path, ""))
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
end
|
|
@@ -27,7 +27,8 @@ module Kward
|
|
|
27
27
|
"git_commit" => "git_commit",
|
|
28
28
|
"read_skill" => "read_skill",
|
|
29
29
|
"ask_user_question" => "ask_user_question",
|
|
30
|
-
"open_editor" => "open_editor"
|
|
30
|
+
"open_editor" => "open_editor",
|
|
31
|
+
"typesafe_evaluate" => "typesafe_evaluate"
|
|
31
32
|
}.freeze
|
|
32
33
|
|
|
33
34
|
module_function
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "base"
|
|
3
|
+
require_relative "../model/typesafe_client"
|
|
4
|
+
|
|
5
|
+
module Kward
|
|
6
|
+
module Tools
|
|
7
|
+
# Evaluates bounded state with TypeSafe's typed decision model.
|
|
8
|
+
class TypeSafeEvaluate < Base
|
|
9
|
+
def initialize(client: TypeSafeClient.new)
|
|
10
|
+
@client = client
|
|
11
|
+
super(
|
|
12
|
+
"typesafe_evaluate",
|
|
13
|
+
"Evaluate text with TypeSafe and return typed decisions, probabilities, and confidence. Use for classification, scoring, routing, or verification; this does not generate text.",
|
|
14
|
+
properties: {
|
|
15
|
+
state: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "The text or structured state to evaluate; do not include secrets unless explicitly requested."
|
|
18
|
+
},
|
|
19
|
+
model: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "TypeSafe model ID or alias; defaults to jev-latest."
|
|
22
|
+
},
|
|
23
|
+
questions: {
|
|
24
|
+
type: "object",
|
|
25
|
+
description: "Named typed questions. Each value must be a noul, choice, or score question.",
|
|
26
|
+
additionalProperties: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
type: { type: "string", enum: %w[noul choice score] },
|
|
30
|
+
instructions: { type: "string" },
|
|
31
|
+
criteria: { type: ["object", "array"] }
|
|
32
|
+
},
|
|
33
|
+
required: %w[type instructions],
|
|
34
|
+
additionalProperties: false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
required: %w[state questions]
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def call(args, _conversation, cancellation: nil)
|
|
43
|
+
state = argument(args, "state").to_s
|
|
44
|
+
questions = argument(args, "questions")
|
|
45
|
+
error = validate(state, questions)
|
|
46
|
+
return "Error: #{error}" if error
|
|
47
|
+
|
|
48
|
+
response = @client.evaluate(
|
|
49
|
+
state: state,
|
|
50
|
+
questions: questions,
|
|
51
|
+
model: argument(args, "model") || TypeSafeClient::DEFAULT_MODEL,
|
|
52
|
+
cancellation: cancellation
|
|
53
|
+
)
|
|
54
|
+
JSON.pretty_generate(response)
|
|
55
|
+
rescue TypeSafeClient::RequestError => e
|
|
56
|
+
"Error: #{e.message}"
|
|
57
|
+
rescue JSON::GeneratorError, TypeError => e
|
|
58
|
+
"Error: invalid TypeSafe response: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def validate(state, questions)
|
|
64
|
+
return "state must be a non-empty string" if state.empty?
|
|
65
|
+
return "state exceeds #{TypeSafeClient::MAX_STATE_BYTES} bytes" if state.bytesize > TypeSafeClient::MAX_STATE_BYTES
|
|
66
|
+
return "questions must be an object" unless questions.is_a?(Hash)
|
|
67
|
+
return "questions must contain 1-#{TypeSafeClient::MAX_QUESTIONS} entries" if questions.empty? || questions.length > TypeSafeClient::MAX_QUESTIONS
|
|
68
|
+
|
|
69
|
+
questions.each do |name, question|
|
|
70
|
+
return "question names must be non-empty strings" unless name.is_a?(String) && !name.empty?
|
|
71
|
+
return "question #{name.inspect} must be an object" unless question.is_a?(Hash)
|
|
72
|
+
type = argument(question, "type").to_s
|
|
73
|
+
return "question #{name.inspect} has unsupported type" unless %w[noul choice score].include?(type)
|
|
74
|
+
return "question #{name.inspect} needs instructions" if argument(question, "instructions").to_s.empty?
|
|
75
|
+
return "question #{name.inspect} needs criteria" if %w[choice score].include?(type) && !argument(question, "criteria")
|
|
76
|
+
end
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Model-callable tool wrappers and their argument schemas.
|
|
4
|
+
module Tools
|
|
5
|
+
# Resolves a small host-scoped set of workspace roles for local tools.
|
|
6
|
+
# Target names are supplied by trusted runtime orchestration, never by the
|
|
7
|
+
# model, so selecting a target cannot escape into an arbitrary filesystem
|
|
8
|
+
# path.
|
|
9
|
+
class WorkspaceTargets
|
|
10
|
+
ACTIVE = "active"
|
|
11
|
+
|
|
12
|
+
def initialize(active:, targets: {})
|
|
13
|
+
@workspaces = { ACTIVE => active }
|
|
14
|
+
targets.each do |name, workspace|
|
|
15
|
+
name = name.to_s
|
|
16
|
+
raise ArgumentError, "Workspace target name is required" if name.empty?
|
|
17
|
+
raise ArgumentError, "Workspace target #{ACTIVE.inspect} is reserved" if name == ACTIVE
|
|
18
|
+
|
|
19
|
+
@workspaces[name] = workspace
|
|
20
|
+
end
|
|
21
|
+
@workspaces.freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def workspace_for(args)
|
|
25
|
+
@workspaces.fetch(target_name(args)) do |name|
|
|
26
|
+
raise ArgumentError, "Unknown workspace target: #{name}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def target_name(args)
|
|
31
|
+
value = if args.respond_to?(:key?)
|
|
32
|
+
args[:target] || args["target"]
|
|
33
|
+
end
|
|
34
|
+
value.to_s.empty? ? ACTIVE : value.to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def valid?(args)
|
|
38
|
+
@workspaces.key?(target_name(args))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def properties(properties)
|
|
42
|
+
return properties if @workspaces.length == 1
|
|
43
|
+
|
|
44
|
+
properties.merge(
|
|
45
|
+
target: {
|
|
46
|
+
type: "string",
|
|
47
|
+
enum: @workspaces.keys,
|
|
48
|
+
description: "Workspace role. Defaults to active; origin is the host repository worktree."
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def without_target(args)
|
|
54
|
+
args.reject { |key, _value| key.to_s == "target" }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -7,15 +7,15 @@ module Kward
|
|
|
7
7
|
# Tool wrapper for guarded full-file writes.
|
|
8
8
|
class WriteFile < Base
|
|
9
9
|
# Builds the tool schema and stores the execution dependency.
|
|
10
|
-
def initialize(workspace:)
|
|
11
|
-
|
|
10
|
+
def initialize(workspace:, workspace_targets: nil)
|
|
11
|
+
configure_workspace_targets(workspace, workspace_targets)
|
|
12
12
|
super(
|
|
13
13
|
"write_file",
|
|
14
14
|
"Write a workspace file. Existing files must be read first.",
|
|
15
|
-
properties:
|
|
15
|
+
properties: targeted_properties(
|
|
16
16
|
path: { type: "string", description: "Workspace-relative path." },
|
|
17
17
|
content: { type: "string", description: "Complete file content." }
|
|
18
|
-
|
|
18
|
+
),
|
|
19
19
|
required: ["path", "content"]
|
|
20
20
|
)
|
|
21
21
|
end
|
|
@@ -25,8 +25,11 @@ module Kward
|
|
|
25
25
|
path = argument(args, :path, "")
|
|
26
26
|
content = argument(args, :content, "")
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
workspace = workspace_for(args)
|
|
29
|
+
result = workspace.write_file(path, content, read_paths: conversation.read_paths)
|
|
30
|
+
if workspace.equal?(@workspace) && agents_file_changed?(workspace, path, result)
|
|
31
|
+
conversation.refresh_system_message!
|
|
32
|
+
end
|
|
30
33
|
result
|
|
31
34
|
end
|
|
32
35
|
end
|