summaryrefslogtreecommitdiffhomepage
path: root/.rules/plan/dispatch-tools-interface-plan.md
blob: bfd3cede6c6465f81df09f2cdcad17a8f493f014 (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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# Dispatch Tools Interface — Gem Implementation Plan

This plan covers the full implementation of the `dispatch-tools-interface` gem.

> **Canonical interface:** See `dispatch-tools-interface-plan.md` in the project root for the finalized, comprehensive interface specification. This plan must conform to that interface.

---

## Overview

This gem provides the framework for defining, registering, and executing AI tools. It is a dependency of every `dispatch-tool-*` gem and is used by the Rails agent loop to discover and invoke tools.

**This gem has zero knowledge of specific tools.** It provides only the framework. Concrete tool gems (files, inquire, test-runner) implement their tools on top of this interface.

---

## Gem Structure

```
dispatch-tools-interface/
├── lib/
│   └── dispatch/
│       └── tools/
│           ├── definition.rb
│           ├── registry.rb
│           ├── result.rb
│           └── errors.rb
├── spec/
│   └── dispatch/
│       └── tools/
│           ├── definition_spec.rb
│           ├── registry_spec.rb
│           └── result_spec.rb
├── dispatch-tools-interface.gemspec
├── Gemfile
├── Rakefile
└── README.md
```

---

## 1. `Dispatch::Tools::Definition`

Declares a tool's metadata and execution logic.

### Construction

```ruby
tool = Dispatch::Tools::Definition.new(
  name: "read_file",
  description: "Read the contents of a file",
  parameters: {
    type: "object",
    properties: {
      path: { type: "string", description: "File path relative to worktree root" },
      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|
  # execution block
  # params = parsed arguments hash (symbolized keys)
  # context = execution context hash (e.g. worktree_path, task_id)
  Dispatch::Tools::Result.success(output: file_contents)
end
```

### Attributes (read-only)

- `name` — String, unique tool identifier (snake_case).
- `description` — String, human-readable description for the LLM.
- `parameters` — Hash, JSON Schema object describing the tool's parameters.

### Methods

- `call(params, context: {})` — Execute the tool's block with the given params and context. Returns a `Dispatch::Tools::Result`. **Never raises** — catches all exceptions from the block and wraps them in `Result.failure`. Validates params via `validate_params` before executing the block; returns `Result.failure` with validation errors if invalid. Symbolizes param keys before passing to the block.
- `to_h` — Returns `{ name:, description:, parameters: }` as a plain hash suitable for passing to an LLM adapter.
- `to_tool_definition` — Returns a hash with the same shape as `to_h`. Used by `Registry#to_a`. (No dependency on the adapter gem — returns a plain hash, not a `Dispatch::Adapter::ToolDefinition` struct.)
- `validate_params(params)` — Validate params against the JSON Schema. Returns `[Boolean, Array<String>]` (valid, error messages). Uses `json_schemer` for full JSON Schema validation.

---

## 2. `Dispatch::Tools::Registry`

Collects tools and provides lookup. Built at agent boot time, then read-only during the agent loop.

### Methods

- `register(tool_definition)` — Add a `Definition` to the registry. Raises `Dispatch::Tools::DuplicateToolError` if a tool with the same name is already registered. Returns `self` for chaining.
- `get(name)` — Look up a tool by name. Returns `Definition` or raises `Dispatch::Tools::ToolNotFoundError`.
- `has?(name)` — Returns `Boolean`.
- `tools` — Returns `Array<Definition>` of all registered tools.
- `tool_names` — Returns `Array<String>` of all registered tool names.
- `to_a` — Returns an `Array<Hash>` where each hash has `{ name:, description:, parameters: }`. These are plain hashes (not adapter structs) — the adapter duck-types on `[:name]` or `.name`.
- `subset(*names)` — Returns a new `Registry` containing only the tools with the given names. Raises `ToolNotFoundError` for any name not found.
- `size` — Returns `Integer`, number of registered tools.
- `empty?` — Returns `Boolean`, true if no tools registered.

### Usage

```ruby
registry = Dispatch::Tools::Registry.new
registry.register(read_file_tool).register(write_file_tool)

# Pass to LLM adapter
adapter.chat(messages, system: system_prompt, tools: registry.to_a)

# Execute a tool call from LLM response
tool = registry.get("read_file")
result = tool.call({ path: "src/main.rb", start_line: 0, end_line: -1 }, context: { worktree_path: "/path/to/worktree" })
```

---

## 3. `Dispatch::Tools::Result`

Standardized return type for tool execution. Immutable after creation.

### Construction

```ruby
# Success
result = Dispatch::Tools::Result.success(output: "file contents here")

# Failure
result = Dispatch::Tools::Result.failure(error: "File not found: src/missing.rb")

# Success with metadata (used by system tools to signal loop control)
result = Dispatch::Tools::Result.success(output: "Gate passed.", metadata: { stop_loop: true })
```

### Attributes (read-only)

- `success?` — Boolean.
- `failure?` — Boolean (inverse of `success?`).
- `output` — String, the tool's output (present on success).
- `error` — String, error message (present on failure).
- `metadata` — Hash, arbitrary metadata (default `{}`). Opaque to the tools interface — consumers (e.g. the agent loop) may inspect it for flags like `stop_loop`. Not sent to the LLM.

### Methods

- `to_s` — Returns `output` on success, `error` on failure. This is what gets sent back to the LLM as the tool result content. `metadata` is not included.
- `to_h` — Returns `{ success: Boolean, output: String?, error: String?, metadata: Hash }`.

---

## 4. Error Classes

Define under `Dispatch::Tools`:

- `Dispatch::Tools::Error` — base error.
- `Dispatch::Tools::DuplicateToolError` — tool name already registered.
- `Dispatch::Tools::ToolNotFoundError` — tool name not in registry.
- `Dispatch::Tools::ValidationError` — parameter validation failed.
- `Dispatch::Tools::ExecutionError` — unhandled error during tool execution.

### Error Design Principle

`call()` never raises. It catches all exceptions (including `ValidationError` and `ExecutionError`) and wraps them in `Result.failure`. The error classes exist for cases where tools are used outside the standard `call()` path (e.g., `validate_params` called directly, or `registry.get()` with a bad name).

---

## 5. Testing

- **Definition tests:**
  - Creating a definition with all attributes.
  - Calling `call()` executes the block and returns a `Result`.
  - `call()` with a failing block returns `Result.failure` (not an exception).
  - `call()` with validation failure returns `Result.failure` with validation error.
  - `to_h` produces the correct shape.
  - `validate_params` correctly validates against the schema.
  - Symbolized keys: verify that string-keyed params are symbolized before passing to block.
- **Registry tests:**
  - Register and retrieve tools.
  - Duplicate registration raises error.
  - Unknown tool lookup raises error.
  - `to_a` produces correct output (array of hashes).
  - `subset` returns a filtered registry.
  - `size` and `empty?` return correct values.
  - Chaining: `registry.register(a).register(b)` works.
- **Result tests:**
  - `success` factory sets correct state.
  - `failure` factory sets correct state.
  - `metadata` defaults to empty hash; can be set.
  - `to_s` returns the right value.
  - `to_h` includes metadata.

---

## 6. Gemspec Dependencies

- `json_schemer` (~> 2.0) for JSON Schema validation of tool parameters.
- No dependency on other dispatch gems. This is the base that others depend on.

---

## Key Constraints

- This gem must have zero knowledge of specific tools. It provides only the framework.
- The `context` hash passed to `call()` is opaque to this gem — tool gems define what they need in context (e.g. `worktree_path`).
- `call()` never raises — all exceptions become `Result.failure`.
- `to_a` returns plain hashes, not adapter structs — no cross-gem dependency.
- `metadata` on Result is opaque to this gem — it exists for consumers (like the agent loop) to use for signaling (e.g. `stop_loop`).
- Thread-safe: `Registry` may be read concurrently. Write operations (register) happen at boot time only.