Class: Kward::ToolRegistry
- Inherits:
-
Object
- Object
- Kward::ToolRegistry
- Defined in:
- lib/kward/tools/registry.rb
Overview
Exposes local workspace, search, skill, and interaction tools to the model and dispatches tool calls into the active conversation.
ToolRegistry is the boundary between model-requested function calls and
Ruby tool objects. It owns schema exposure and transcript persistence for
tool results; individual tools own validation and side effects. Keep frontend
policy outside this class by passing dependencies such as workspace and
prompt from CLI or RPC setup.
A tool may exist in @tools but not be advertised in schemas. This allows
restored transcripts or compatibility callers to dispatch known tools while
config and frontend capability checks decide what the model can request next.
Tool schemas are the strict output contract advertised to models and clients. Incoming calls are intentionally more tolerant: extra fields are ignored by individual tools, and legacy-compatible shapes are accepted where already supported. Required fields and invalid required values should still return explicit tool errors.
Constant Summary collapse
- CORE_TOOL_NAMES =
%w[ list_directory read_file write_file edit_file run_shell_command code_search summarize_file_structure context_for_task context_budget_stats retrieve_tool_output ].freeze
- TARGETED_WORKSPACE_TOOLS =
%w[ list_directory read_file write_file edit_file run_shell_command summarize_file_structure context_for_task git_commit ].freeze
- BUILTIN_TOOL_NAMES =
(CORE_TOOL_NAMES + %w[ git_commit web_search fetch_content fetch_raw read_skill ask_user_question replace_editor_buffer open_editor prepare_shell_command typesafe_evaluate ]).freeze
Instance Attribute Summary collapse
-
#schemas ⇒ Array<Hash>
readonly
Tool schemas advertised to the model for the current frontend and config.
Instance Method Summary collapse
-
#dispatch(tool_call, conversation, cancellation: nil) ⇒ String
Executes a model-requested tool call and appends the result to the conversation transcript.
-
#for_editor_prompt(editor_prompt_session) ⇒ Object
Builds a registry scoped to one editor prompt turn.
-
#for_shell_prompt(shell_prompt_session) ⇒ Object
Builds a registry scoped to one transient shell-agent turn.
-
#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) ⇒ ToolRegistry
constructor
Builds tool objects and the schema list for the current frontend/config.
-
#metadata_for(name) ⇒ Hash
Returns frontend discovery metadata for a tool name.
-
#replace_plugin_tools!(plugin_tools) ⇒ Object
Replaces plugin registrations while preserving this registry's frontend, workspace, permission, hook, and execution-profile restrictions.
Constructor Details
#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) ⇒ ToolRegistry
Builds tool objects and the schema list for the current frontend/config.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/kward/tools/registry.rb', line 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) @workspace = workspace @workspace_targets = workspace_targets || Tools::WorkspaceTargets.new(active: workspace) @prompt = prompt @web_search = web_search @web_fetch = web_fetch @code_search = code_search @typesafe_client = typesafe_client @skills = skills @web_search_enabled = web_search_enabled @ask_user_question_enabled = ask_user_question_enabled @allowed_tool_names = allowed_tool_names&.map(&:to_s) @editor_prompt_session = editor_prompt_session @shell_prompt_session = nil @tool_output_compactor = tool_output_compactor @telemetry_logger = telemetry_logger @context_budget_meter = context_budget_meter @plugin_tools = Array(plugin_tools) @tool_approval = tool_approval @approval_for_allowed_tools = approval_for_allowed_tools == true @permission_policy = || Permissions::Policy.from_config(ConfigFiles.read_config) @hook_manager = hook_manager @hook_context = hook_context @git_committer = git_committer @mcp_clients = if mcp_clients mcp_clients elsif @allowed_tool_names [] else MCP::ServerConfig.clients_from_config(ConfigFiles.read_config) end @tools = build_tools.freeze @schemas = build_schema_tools.map { |tool| (tool) }.freeze end |
Instance Attribute Details
#schemas ⇒ Array<Hash> (readonly)
Tool schemas advertised to the model for the current frontend and config.
99 100 101 |
# File 'lib/kward/tools/registry.rb', line 99 def schemas @schemas end |
Instance Method Details
#dispatch(tool_call, conversation, cancellation: nil) ⇒ String
Executes a model-requested tool call and appends the result to the conversation transcript.
Unknown tools are recorded as tool results instead of raising. That keeps the conversation valid for the model and lets the assistant recover by choosing an advertised tool on the next turn.
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/kward/tools/registry.rb', line 199 def dispatch(tool_call, conversation, cancellation: nil) cancellation&.raise_if_cancelled! name = ToolCall.name(tool_call) args = ToolCall.arguments(tool_call) tool = @tools[name] original_content = if tool before_tool = run_hook("tool_call_before", conversation, payload: tool_payload(name, args, tool_call)) args = hook_arguments(before_tool, args) target_error = workspace_target_error(name, args) if target_error target_error elsif before_tool.denied? hook_denied_content(before_tool, "tool call denied: #{name}") elsif before_tool.approval_required? && hook_approval_denied?(before_tool, tool_call, name, args, cancellation) hook_denied_content(before_tool, "tool call approval denied: #{name}") else = @permission_policy.decision_for(name, args, source: source_for_tool(tool)) if .denied? "Declined: #{.reason}: #{name}" elsif .approval_required? approval = (tool_call, name, args, cancellation) if approval == :allow_for_session @permission_policy.allow_for_session!(name) execute_tool_with_hooks(tool, name, args, tool_call, conversation, cancellation) elsif approval == true execute_tool_with_hooks(tool, name, args, tool_call, conversation, cancellation) else approval_denied_content(approval, name) end elsif @approval_for_allowed_tools && tool_approval_denied?(tool_call, name, args, cancellation) "Declined: tool execution denied by user: #{name}" else execute_tool_with_hooks(tool, name, args, tool_call, conversation, cancellation) end end else "Unknown tool: #{name}" end original_content = Conversation.normalize_tool_content(original_content) duplicate_id = conversation.tool_output_artifact_id_for(tool_name: name, content: original_content) content = original_content if reusable_duplicate_output?(name) && conversation.tool_output_artifacts.key?(duplicate_id) content = "[Same as previous tool output #{duplicate_id}; not repeated. Use retrieve_tool_output to inspect it.]" end artifact_id = nil model_content = compact_tool_output(name, content, original_content, conversation) do artifact_id ||= conversation.store_tool_output_artifact(tool_name: name, content: original_content) end record_context_budget(conversation, name, before: original_content, after: model_content) log_tool_output_compaction(name, artifact_id: artifact_id, before: original_content, after: model_content) if model_content != original_content conversation.append_tool( tool_call_id: tool_call["id"] || tool_call[:id], name: name, content: model_content ) conversation.append_tool_execution(tool_call: tool_call, content: original_content) model_content end |
#for_editor_prompt(editor_prompt_session) ⇒ Object
Builds a registry scoped to one editor prompt turn.
Editor prompts may inspect the workspace, but their only write operation is the in-memory editor buffer. Filesystem and shell mutation tools are intentionally excluded from this scoped registry.
164 165 166 167 168 169 170 171 172 |
# File 'lib/kward/tools/registry.rb', line 164 def for_editor_prompt(editor_prompt_session) registry = dup registry.instance_variable_set(:@editor_prompt_session, editor_prompt_session) registry.instance_variable_set(:@shell_prompt_session, nil) registry.instance_variable_set(:@allowed_tool_names, editor_prompt_tool_names) registry.instance_variable_set(:@tools, registry.send(:build_tools).freeze) registry.instance_variable_set(:@schemas, registry.send(:build_schema_tools).map { |tool| registry.send(:schema_with_metadata, tool) }.freeze) registry end |
#for_shell_prompt(shell_prompt_session) ⇒ Object
Builds a registry scoped to one transient shell-agent turn.
Shell prompts may inspect the workspace and execute explicitly requested commands through the active shell, but they cannot write workspace files through the ordinary file tools or execute against a second shell state.
179 180 181 182 183 184 185 186 187 |
# File 'lib/kward/tools/registry.rb', line 179 def for_shell_prompt(shell_prompt_session) registry = dup registry.instance_variable_set(:@editor_prompt_session, nil) registry.instance_variable_set(:@shell_prompt_session, shell_prompt_session) registry.instance_variable_set(:@allowed_tool_names, shell_prompt_tool_names) registry.instance_variable_set(:@tools, registry.send(:build_tools).freeze) registry.instance_variable_set(:@schemas, registry.send(:build_schema_tools).map { |tool| registry.send(:schema_with_metadata, tool) }.freeze) registry end |
#metadata_for(name) ⇒ Hash
Returns frontend discovery metadata for a tool name.
Unknown names return metadata with an unknown source rather than raising, which lets RPC clients render restored or unsupported tool calls safely.
269 270 271 272 273 274 |
# File 'lib/kward/tools/registry.rb', line 269 def (name) tool = @tools[name.to_s] return (name) unless tool (tool) end |
#replace_plugin_tools!(plugin_tools) ⇒ Object
Replaces plugin registrations while preserving this registry's frontend, workspace, permission, hook, and execution-profile restrictions.
151 152 153 154 155 156 157 |
# File 'lib/kward/tools/registry.rb', line 151 def replace_plugin_tools!(plugin_tools) @plugin_tools = Array(plugin_tools) @plugin_tool_values = nil @tools = build_tools.freeze @schemas = build_schema_tools.map { |tool| (tool) }.freeze self end |