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.
Instance Attribute Summary collapse
-
#schemas ⇒ Array<Hash>
readonly
Tool schemas advertised to the model for the current frontend and config.
-
#writer_id ⇒ 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.
-
#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, write_lock: nil, writer_id: nil, tool_output_compactor: ToolOutputCompactor.new, telemetry_logger: TelemetryLogger.new, context_budget_meter: nil, mcp_clients: nil, tool_approval: nil, hook_manager: nil, hook_context: nil) ⇒ ToolRegistry
constructor
Builds tool objects and the schema list for the current frontend/config.
- #metadata_for(name) ⇒ Object
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, write_lock: nil, writer_id: nil, tool_output_compactor: ToolOutputCompactor.new, telemetry_logger: TelemetryLogger.new, context_budget_meter: nil, mcp_clients: nil, tool_approval: nil, hook_manager: nil, hook_context: nil) ⇒ ToolRegistry
Builds tool objects and the schema list for the current frontend/config.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/kward/tools/registry.rb', line 66 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, write_lock: nil, writer_id: nil, tool_output_compactor: ToolOutputCompactor.new, telemetry_logger: TelemetryLogger.new, context_budget_meter: nil, mcp_clients: nil, tool_approval: nil, hook_manager: nil, hook_context: 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) @write_lock = write_lock @writer_id = writer_id @tool_output_compactor = tool_output_compactor @telemetry_logger = telemetry_logger @context_budget_meter = context_budget_meter @tool_approval = tool_approval @hook_manager = hook_manager @hook_context = hook_context @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.
53 54 55 |
# File 'lib/kward/tools/registry.rb', line 53 def schemas @schemas end |
#writer_id ⇒ Array<Hash> (readonly)
Tool schemas advertised to the model for the current frontend and config.
53 54 55 |
# File 'lib/kward/tools/registry.rb', line 53 def writer_id @writer_id 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.
105 106 107 108 109 110 111 112 113 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 148 149 |
# File 'lib/kward/tools/registry.rb', line 105 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}") elsif mutation_tool?(name) && !write_lock_owned? "Workspace write denied: another worker owns the write lock." elsif 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 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 |
#metadata_for(name) ⇒ Object
151 152 153 154 155 156 |
# File 'lib/kward/tools/registry.rb', line 151 def (name) tool = @tools[name.to_s] return (name) unless tool (tool) end |