summaryrefslogtreecommitdiffhomepage
path: root/README.md
blob: e08bdd0afa4525a001884dee9a4ebc42fbc86f27 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# Dispatch::Tools::Interface

A Ruby gem that provides a structured interface for defining, validating, and executing tool definitions. Tools are defined with JSON Schema parameter validation (via [json_schemer](https://github.com/davishmcclurg/json_schemer)) and organized in a registry for lookup and execution.

Designed as a building block for systems that expose callable tools — such as AI agent frameworks, plugin architectures, or RPC-style APIs.

### Core Components

| Class | Purpose |
|---|---|
| `Dispatch::Tools::Definition` | Defines a single tool: name, description, JSON Schema parameters, and an execution block |
| `Dispatch::Tools::Registry` | Stores and retrieves tool definitions by name |
| `Dispatch::Tools::Result` | Immutable value object representing success or failure of a tool call |

---

### Installation

Add to your Gemfile:

```ruby
gem "dispatch-tools-interface"
```

Then run:

```bash
bundle install
```

Or install directly:

```bash
gem install dispatch-tools-interface
```

---

### Usage

#### Defining a Tool

Create a `Definition` with a name, description, a JSON Schema hash for parameters, and a block that receives validated params and an optional context hash:

```ruby
read_file = Dispatch::Tools::Definition.new(
  name: "read_file",
  description: "Read the contents of a file",
  parameters: {
    type: "object",
    properties: {
      path: { type: "string", description: "File path" },
      start_line: { type: "integer", description: "Start line (0-based)" },
      end_line: { type: "integer", description: "End line (0-based, -1 for EOF)" }
    },
    required: ["path"]
  }
) do |params, context|
  content = File.read(params[:path])
  Dispatch::Tools::Result.success(output: content)
end
```

#### Calling a Tool

Pass a params hash (string or symbol keys) and an optional `context` keyword argument. Parameters are validated against the JSON Schema before the block executes. If validation fails, a `Result.failure` is returned — the block is never called. Exceptions raised inside the block are caught and wrapped in a failure result.

```ruby
result = read_file.call({ path: "src/main.rb" })

result.success?  # => true
result.output    # => "contents of src/main.rb"
result.to_s      # => "contents of src/main.rb"

# With context
result = read_file.call({ path: "src/main.rb" }, context: { worktree_path: "/tmp/work" })

# Validation failure
result = read_file.call({})  # missing required "path"
result.failure?  # => true
result.error     # => "Parameter validation failed: ..."
```

#### Working with Results

`Result` is an immutable (frozen) value object with two factory methods:

```ruby
success = Dispatch::Tools::Result.success(output: "done", metadata: { elapsed: 0.3 })
success.success?   # => true
success.output     # => "done"
success.metadata   # => { elapsed: 0.3 }
success.to_h       # => { success: true, output: "done", error: nil, metadata: { elapsed: 0.3 } }

failure = Dispatch::Tools::Result.failure(error: "File not found")
failure.failure?   # => true
failure.error      # => "File not found"
failure.to_s       # => "File not found"
```

#### Using the Registry

The `Registry` stores tool definitions by name and supports lookup, listing, and subsetting:

```ruby
registry = Dispatch::Tools::Registry.new

registry.register(read_file)
registry.register(write_file)
registry.register(delete_file)

# Lookup
tool = registry.get("read_file")
result = tool.call({ path: "README.md" })

# Query
registry.has?("read_file")  # => true
registry.tool_names         # => ["read_file", "write_file", "delete_file"]
registry.size               # => 3
registry.empty?             # => false

# Export all definitions as an array of hashes
registry.to_a
# => [{ name: "read_file", description: "...", parameters: { ... } }, ...]

# Create a subset registry with only specific tools
subset = registry.subset("read_file", "write_file")
subset.tool_names  # => ["read_file", "write_file"]
```

Registration is chainable:

```ruby
registry.register(tool_a).register(tool_b).register(tool_c)
```

#### Error Handling

All custom errors inherit from `Dispatch::Tools::Error`:

| Error | Raised when |
|---|---|
| `DuplicateToolError` | Registering a tool with a name that already exists |
| `ToolNotFoundError` | Calling `Registry#get` or `Registry#subset` with an unknown name |
| `ValidationError` | Available for custom validation logic |
| `ExecutionError` | Available for custom execution error handling |

---

### Development

After checking out the repo, run `bin/setup` to install dependencies. Then run the test suite:

```bash
bundle exec rake spec
```

Use `bin/console` for an interactive prompt to experiment with the library.

---

### License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).