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 inside execute to stop the conversation loop.
  • StandardError — Caught by call and returned as error Ask::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


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

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).


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

This site uses Just the Docs, a documentation theme for Jekyll.