Skip to content
Kward Search API index

Class: Kward::ToolRegistry

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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) ⇒ ToolRegistry

Builds tool objects and the schema list for the current frontend/config.

Parameters:

  • workspace (Workspace) (defaults to: Workspace.new)

    filesystem/shell boundary used by local tools

  • prompt (Object, nil) (defaults to: nil)

    interactive prompt bridge; must implement ask_user_question before that tool is advertised

  • web_search (WebSearch) (defaults to: WebSearch.new)

    live web search implementation

  • web_fetch (WebFetch) (defaults to: WebFetch.new)

    specific URL fetch implementation

  • code_search (CodeSearch) (defaults to: CodeSearch.new)

    public source/package search implementation

  • web_search_enabled (Boolean, nil) (defaults to: nil)

    override for web search exposure

  • skills (Array<ConfigFiles::Skill>, nil) (defaults to: nil)

    override discovered skills

  • ask_user_question_enabled (Boolean, nil) (defaults to: nil)

    override question exposure



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/kward/tools/registry.rb', line 63

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)
  @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
  @tools = build_tools.freeze
  @schemas = build_schema_tools.map(&:schema).freeze
end

Instance Attribute Details

#schemasArray<Hash> (readonly)

Tool schemas advertised to the model for the current frontend and config.

Returns:

  • (Array<Hash>)

    tool schemas currently advertised to the model



50
51
52
# File 'lib/kward/tools/registry.rb', line 50

def schemas
  @schemas
end

#writer_idArray<Hash> (readonly)

Tool schemas advertised to the model for the current frontend and config.

Returns:

  • (Array<Hash>)

    tool schemas currently advertised to the model



50
51
52
# File 'lib/kward/tools/registry.rb', line 50

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.

Parameters:

  • tool_call (Hash)

    model tool call payload

  • conversation (Conversation)

    active conversation

Returns:

  • (String)

    tool output content appended to the conversation



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/kward/tools/registry.rb', line 92

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
                       if mutation_tool?(name) && !write_lock_owned?
                         "Workspace write denied: another worker owns the write lock."
                       else
                         tool.call(args, conversation, cancellation: 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 = @tool_output_compactor.compact(name, content) 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