Home
Home/s02
s0280 lines of code

Tool Dispatch

Register Once, Use Everywhere

Key Insight:

The loop stays stable while capabilities register into a dispatch table.

A dispatch table, TOOL_MAP, maps tool names to handler functions. Rather than maintaining a growing if-else chain, the loop performs a single map lookup. Adding a new tool requires just one line: registering the name and its handler in the map. The loop code itself never changes.

Architecture Flow

The Problem: How to Add New Tools Without Changing the Loop

If tool execution logic were inlined in the loop, every new tool would require modifying the loop body, adding an elif branch, a new import, and special-case handling. Over time, the loop accumulates an unwieldy set of branches, making it difficult to reason about and fragile to modify. The solution is a dispatch table: a dictionary that maps tool names to handler functions. The loop calls execute_tool(name, input) and never needs to know which tool actually runs. Adding a new tool becomes a one-line registration.

The Open/Closed Principle in Practice

The dispatch table embodies the Open/Closed Principle: the loop is closed for modification but open for extension. The loop never changes. New capabilities arrive as new entries in TOOL_MAP. This pattern also simplifies testing: each handler is an independently testable function, and the dispatch logic itself is a trivial dictionary lookup that is easily verified. The handler signature is standardized, accepting an input dict and returning an output string, so any function fitting this shape can serve as a tool.

Real Implementation: Two-Tier Registration

OpenCode's actual tool system (packages/core/src/tool/) uses a two-tier registration model rather than a single flat map. Application-level tools, which are user-facing and process-global, are registered via ApplicationTools.Service using State.Transformable. Per-location tools, which are directory-scoped, are registered separately and override application tools with the same name. All tools are canonical, created via Tool.make() which returns an opaque Definition with input/output schemas and an executor. The Registry.materialize() step derives tool definitions, applies permission filters, and produces a settle function that the loop calls. This design means the same tool abstraction works for builtins, MCP servers, and plugins. There is no separate path for external tools.

Design Decisions

The dispatch map uses `dict.get(name)` with a fallback to `None` rather than `dict[name]`, which would raise a KeyError. This means unknown tool names produce a friendly error message instead of crashing the agent.

Handlers receive `**input`, unpacked keyword arguments, meaning each tool defines its own parameter schema. The dispatch table does not need to know or validate parameters. That responsibility belongs to the handler.

Comparison: Claude Code

Both systems use dispatch table and registry patterns. Claude Code provides a built-in tool set with extensibility through MCP servers. OpenCode's TOOL_MAP is conceptually similar but is designed for first-class extensibility: the tool registry is a formal data structure with ToolID, schema, and handler rather than an implicit if-else chain. New tools register themselves with metadata, enabling features such as wildcard permission rules and typed argument validation.

Deep Dive: Design Decisions

Dispatch Map over If-Else Chain

A dispatch map (Record<string, ToolHandler>) turns tool routing into a single map lookup instead of a growing if-else chain. Adding a new tool means registering it in the map — the loop code never changes.

Alternatives: An if-else chain works for 2-3 tools but doesn't scale. The dispatch map pattern keeps the loop stable regardless of how many tools are added.

Uniform Handler Signature

Every tool handler follows the same pattern: parse typed input, execute, return a result. This uniformity lets the dispatch loop treat all tools identically — it doesn't need to know what each tool does internally.

Alternatives: Tools could return different shapes, but that would force the loop to handle each tool's output format differently. Uniformity is worth the slight abstraction cost.

Learn OpenCode — Built with Next.js