Quick Start
require "ask-tools"
class Greeter < Ask::Tool
description "Greets a person by name"
param :name, type: :string, desc: "The person's name", required: true
def execute(name:)
Ask::Result.ok(data: "Hello, #{name}!")
end
end
tool = Greeter.new
tool.name # => "greeter"
tool.description # => "Greets a person by name"
result = tool.call(name: "World")
result.ok? # => true
result.output # => "Hello, World!"
Ask::Tool — Base Class
Subclass Ask::Tool to define a tool that an LLM can call.
Class DSL
| Method | Description |
|---|---|
description(text) | Sets/retrieves the tool’s description. Alias: desc |
name(text) | Override the auto-derived tool name. Call with no argument returns the class path |
param(name, type:, desc:, required:) | Declares a parameter. type must be a valid JSON Schema type |
Instance Methods
| Method | Returns | Description |
|---|---|---|
name | String | Returns custom name if set via name "" DSL, otherwise auto-derived from class name (CamelCase → snake_case, strips _tool) |
description | String? | The tool’s description |
parameters | Hash{Symbol => Parameter} | Declared parameter definitions |
call(args = {}) | Ask::Result | Normalizes args, validates, delegates to execute |
execute(**args) | Ask::Result | Override this. Implement the tool’s logic |
params_schema | Hash? | JSON Schema hash for LLM function-calling APIs |
tool_definition | Hash | Full tool definition with name, description, input_schema |
Error Handling
Ask::Tool::Halt— Raise insideexecuteto stop the conversation loop.StandardError— Caught bycalland returned as errorAsk::Result.
Ask::Result — Standardized Return Value
# Factories
Ask::Result.ok(data: "output")
Ask::Result.error(message: "fail")
# Attributes
result.ok? # => true / false
result.output # => "output"
result.error # => nil / "fail"
result.metadata # => {}
result.to_s # => human-readable
result.to_h # => { ok: true, output: "...", error: nil, metadata: {} }
Ask::Tools — Registry & Discovery
Ask::Tools.register(MyTool)
Ask::Tools.all # => [MyTool.new, ...]
Ask::Tools.discover # auto-discover loaded Ask::Tool subclasses
Ask::Tools["my_tool"] # find by derived name
Ask::Tools.count # => 1
Ask::Tools.clear # reset
Thread-safe via Monitor.
Writing Custom Tools
class SearchTool < Ask::Tool
description "Searches a knowledge base"
param :query, type: :string, desc: "Search query", required: true
param :limit, type: :integer, desc: "Max results", required: false
def execute(query:, limit: 10)
results = perform_search(query, limit)
Ask::Result.ok(data: results)
rescue SearchError => e
Ask::Result.error(message: e.message)
end
end
Links
- Source: github.com/ask-rb/ask-tools
- Issues: github.com/ask-rb/ask-tools/issues
- Rubygems: rubygems.org/gems/ask-tools
ask-tools-shell
Shell, filesystem, and code execution tools. Ships 7 tools every agent needs: Bash, Read, Write, Edit, Glob, Grep, and Code.
gem "ask-tools-shell"
Quick Start
require "ask-tools-shell"
Ask::Tools::Shell.all.map(&:name)
# => ["bash", "read", "write", "edit", "glob", "grep", "code"]
Ask::Tools::Bash.new.call(command: "echo hello")
Ask::Tools::Read.new.call(path: "/etc/hosts")
Ask::Tools::Code.new.call(code: "puts RUBY_VERSION")
Sandbox Configuration (v0.2.0+)
Both Bash and Code tools use Ask::Sandbox.provider from the ask-sandbox-providers gem. By default, execution happens in a local subprocess with resource limits. To enable stronger isolation:
require "ask-sandbox-providers"
# Docker containers
Ask::Sandbox.provider = Ask::Sandbox::Docker.new(
image: "ruby:3.4-alpine",
memory: "256m",
network: false
)
# Remote sandboxes via Daytona
Ask::Sandbox.provider = Ask::Sandbox::Daytona.new(
api_key: ENV["DAYTONA_API_KEY"]
)
# Cloudflare Workers sandbox
Ask::Sandbox.provider = Ask::Sandbox::Cloudflare.new(
worker_url: "https://sandbox-proxy.my-worker.workers.dev"
)
When a command times out, Bash and Code return Ask::Result.error instead of Ask::Result.ok.
Available Tools
| Tool | Params | Description |
|---|---|---|
| Bash | command (req), timeout, workdir | Execute shell commands in a sandboxed temp dir. Returns stdout, stderr, exit_code, timed_out. Output truncated to 100KB |
| Read | path (req), offset, limit | Read files with line numbers or list directories. Default limit 2000 lines |
| Write | path (req), content (req) | Write to files, creating parent dirs automatically. Max 500KB |
| Edit | path (req), old_string (req), new_string (req), replace_all | Replace exact text. Single replacement by default |
| Glob | pattern (req), path | Find files matching glob. Max 1000 results, sorted newest first |
| Grep | pattern (req), path, include | Regex search in files. Max 100 matches, 500 chars/line. Skips .git, node_modules, etc. |
| Code | code (req) | Execute Ruby in a subprocess. Uses available gems, passes env through |
Links
- Source: github.com/ask-rb/ask-tools-shell
- Rubygems: rubygems.org/gems/ask-tools-shell
Ask::Tools::SubAgent — Sub-Agent Delegate Tool
New in ask-tools 0.3.0
A tool that delegates a task to a specialized sub-agent. When the coordinator LLM calls it, a fresh sub-agent session runs independently with its own model, tools, and instructions.
require "ask-tools"
search = Ask::Tools::SubAgent.new(
name: "web_search",
description: "Search the web for current information",
runner: ->(task) {
Ask::Agent::Session.new(model: "gpt-4o-mini", tools: [Search])
.run(task).to_s
}
)
search.call(task: "Latest Rails release")
# => "Rails 8.0 was released on..."
The name and description can be set per-instance, so multiple sub-agents can coexist in the same tool list:
search = Ask::Tools::SubAgent.new(name: "web_search", runner: ...)
review = Ask::Tools::SubAgent.new(name: "code_review", runner: ...)
analyze = Ask::Tools::SubAgent.new(name: "data_analysis", runner: ...)
Use Ask::Agent.sub_agent_tool to create these from a session factory without writing the runner lambda manually (see The Agent Loop).
ask-web-search
Web search tool powered by SearXNG. Provides a single tool — Ask::Tools::WebSearch — that searches the web via a local SearXNG instance and returns formatted results for LLM consumption.
gem "ask-web-search"
Quick Start
require "ask/web_search"
tool = Ask::Tools::WebSearch.new
result = tool.execute(query: "ruby programming language")
puts result
# => 1. Ruby — A Programmer's Best Friend
# https://www.ruby-lang.org
# Ruby is a dynamic, open-source programming language...
Configuration
Point to your SearXNG instance via the SEARXNG_URL environment variable:
export SEARXNG_URL=http://localhost:8888
Defaults to http://localhost:8888.
Start SearXNG with Docker:
docker run -d --name searxng -p 8888:8080 searxng/searxng
Usage with Chat
chat = Ask::Agent::Chat.new(
model: "deepseek-v4-flash",
tools: [Ask::Tools::WebSearch.new]
)
chat.ask("What is the population of Tokyo? Search the web.")
Usage with Agent
agent = Ask::Agent.new(tools: [Ask::Tools::WebSearch])
agent.run("Find recent news about Mars exploration.")
Available Tools
| Tool | Params | Description |
|---|---|---|
| WebSearch | query (req) | Searches the web via SearXNG. Returns numbered results with title, URL, and content. Includes infobox results. Deduplicates by URL |
API
| Method | Returns | Description |
|---|---|---|
execute(query:) | String | Searches the web and returns formatted results, or "No results found." |
Output Format
Results are returned as a numbered markdown-like string:
1. Ruby — A Programmer's Best Friend
https://www.ruby-lang.org
Ruby is a dynamic, open-source programming language...
2. Ruby on Rails
https://rubyonrails.org
Rails is a web application framework...
Development
bundle install
bundle exec rake test # 12 tests, 25 assertions
Requires a running SearXNG instance for the integration test.
Dependencies
- Runtime:
ask-tools >= 0.1 - Zero of our own runtime deps — clean dependency tree
Links
- Source: github.com/ask-rb/ask-web-search
- Rubygems: rubygems.org/gems/ask-web-search