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
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, 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, 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.
Constructor Details
#initialize(workspace: Workspace.new, 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, 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.
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/kward/tools/registry.rb', line 85 def initialize(workspace: Workspace.new, 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, tool_approval: nil, approval_for_allowed_tools: false, permission_policy: nil, hook_manager: nil, hook_context: nil, git_committer: nil) @workspace = workspace @prompt = prompt @web_search = web_search @web_fetch = web_fetch @code_search = code_search @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 @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.
72 73 74 |
# File 'lib/kward/tools/registry.rb', line 72 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.
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/kward/tools/registry.rb', line 157 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) if 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.
122 123 124 125 126 127 128 129 130 |
# File 'lib/kward/tools/registry.rb', line 122 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.
137 138 139 140 141 142 143 144 145 |
# File 'lib/kward/tools/registry.rb', line 137 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.
224 225 226 227 228 229 |
# File 'lib/kward/tools/registry.rb', line 224 def (name) tool = @tools[name.to_s] return (name) unless tool (tool) end |