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 datetime ]).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.
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 148 149 |
# File 'lib/kward/tools/registry.rb', line 116 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.
101 102 103 |
# File 'lib/kward/tools/registry.rb', line 101 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.
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 260 261 |
# File 'lib/kward/tools/registry.rb', line 201 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.
166 167 168 169 170 171 172 173 174 |
# File 'lib/kward/tools/registry.rb', line 166 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.
181 182 183 184 185 186 187 188 189 |
# File 'lib/kward/tools/registry.rb', line 181 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.
271 272 273 274 275 276 |
# File 'lib/kward/tools/registry.rb', line 271 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.
153 154 155 156 157 158 159 |
# File 'lib/kward/tools/registry.rb', line 153 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 |