Unified Contract: Every Tool Plays by the Same Rules
What is a "contract"? Imagine you're hiring a courier (a tool). Whether it's FedEx or UPS, as the sender you have the same expectations: tell me if it can deliver, have me sign the right authorization before delivery, update me on progress in real time, tell me the result when done. These expectations are the "contract" — it defines the rights and obligations of both parties. The employer (Agent Loop) doesn't need to know how each courier company is internally organized; it just calls according to the contract.
src/Tool.ts defines the foundational type for the entire tool system. Every tool — whether the built-in BashTool or a user-connected MCP tool — must implement this interface.
The core of the interface is the buildTool() function:
// src/Tool.ts
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return {
...TOOL_DEFAULTS,
userFacingName: () => def.name,
...def,
} as BuiltTool<D>
}
buildTool() acts like a factory function: tool developers only provide the parts that differ; the rest is filled in by TOOL_DEFAULTS. The defaults follow the fail-closed principle:
// src/Tool.ts
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?) => false, // Default: no concurrency
isReadOnly: (_input?) => false, // Default: assumes writes
isDestructive: (_input?) => false, // Default: assumes reversible
checkPermissions: (input, _ctx?) =>
Promise.resolve({ behavior: 'allow', updatedInput: input }), // Default: allow
toAutoClassifierInput: (_input?) => '', // Default: not part of safety classification
userFacingName: (_input?) => '',
}
The design is deliberately cautious: isConcurrencySafe defaults to false, meaning new tools disallow concurrent execution by default. A tool must explicitly declare itself concurrency-safe before the Agent Loop will run it in parallel.
What does "explicit declaration" mean? The tool developer actively writes isConcurrencySafe: () => true in the code, telling the system "I support concurrency." Compare with implicit — writing nothing, so the system takes the default value:
| Implicit (write nothing) | Explicit (active declaration) | |
|---|---|---|
| Code form | Omit isConcurrencySafe field | Write isConcurrencySafe: () => true |
| System behavior | Takes TOOL_DEFAULTS default of false — no concurrency | System knows this tool can run alongside others |
| Typical example | New tool just created, developer unsure if it's safe | GlobTool, GrepTool (read-only searches, naturally concurrency-safe) |
This embodies the "fail-closed" principle: it's better to conservatively serialize execution than to risk data races from concurrency. Only when a developer stands up and says "I'm fine" does the system allow concurrency.