Abhed — Tool Contracts

Status: Draft · 2026-09-02 Evidence status: [E] engineering judgment, informed by verified finding P2 (tool surface discipline: 15 tools → 2 moved success 80% → 100%, vendor-reported, medium confidence).

This is the most important document in the repo. Tools are where agents actually fail — not in the loop, not in the model. A tool that returns an unhelpful error teaches the model nothing and burns a turn; a tool with ambiguous semantics produces silent corruption.

0. Rules that apply to every tool

  1. Errors are instructions. An error message is read by the model, not a human. It must say what failed, why, and what to do instead. "File not found" is a wasted turn; "File not found: src/auth.ts. Did you mean src/auth/index.ts? Use glob to list." is a recovered one.
  2. No silent success. A tool that "worked" but changed nothing must say so. The most expensive failure mode is an edit that no-ops while reporting success — the model proceeds on a false premise and the error surfaces many turns later.
  3. Deterministic output shape. Same input → same output format, always. The model learns the shape; violating it costs accuracy.
  4. Bounded output. Every tool caps its response and says when it truncated. Unbounded output is a context-budget attack on yourself (P2).
  5. Absolute paths in, absolute paths out. Relative paths are ambiguous across turns after a cd. Normalize at the boundary; reject relative paths with a message showing the absolute form.
  6. Idempotent where possible. Re-running a tool after an ambiguous failure must be safe.
  7. Every call is an event (P6): ActionEvent in, ObservationEvent out, both persisted.

1. Tool surface

Nine native tools. Everything else arrives via the reviewed MCP gateway (§03-security). This set is deliberately small — every tool is an attack surface and a decision the model can get wrong.

ToolPurposeMutatesApproval
readRead file contentnonone
writeCreate/overwrite fileyesask (unless allowlisted)
editExact-match replacementyesask (unless allowlisted)
globFind files by patternnonone
grepSearch file contentsnonone
bashExecute shell commandyesask, per-command scoped
taskSpawn subagentno*none (budget-capped)
planWrite/update task plannonone
todoTrack multi-step progressnonone

\* task doesn't mutate directly, but its subagent can.


2. read

{
  "name": "read",
  "description": "Read a file from the filesystem. Returns numbered lines. Prefer this over `cat` via bash — output is bounded, numbered, and the read is tracked for edit safety.",
  "input_schema": {
    "type": "object",
    "properties": {
      "path":   {"type":"string","description":"Absolute path to the file."},
      "offset": {"type":"integer","description":"1-indexed line to start from. Use with limit for large files."},
      "limit":  {"type":"integer","description":"Max lines to return. Default 2000."}
    },
    "required": ["path"]
  }
}

Semantics

Errors

ConditionMessage
MissingFile not found: {path}. Use glob to locate it, or check the parent directory exists.
Is a directory{path} is a directory, not a file. Use glob("{path}/**") to list contents.
No permissionPermission denied reading {path}.
Outside workspace{path} is outside the session workspace ({root}). Access denied.
Relative pathPath must be absolute. Did you mean {cwd}/{path}?

3. write

{
  "name": "write",
  "description": "Write content to a file, creating it or overwriting it entirely. For modifying part of an existing file, use `edit` instead — it is safer and cheaper.",
  "input_schema": {
    "type":"object",
    "properties":{
      "path":    {"type":"string","description":"Absolute path."},
      "content": {"type":"string","description":"Complete file content."}
    },
    "required":["path","content"]
  }
}

Semantics

Errors

ConditionMessage
Overwrite without readRefusing to overwrite {path} — it exists but has not been read this session. Call read({path}) first to see what you would replace.
Parent missingParent directory does not exist: {dir}. Create it with bash mkdir -p first.
Read-only FSFilesystem is read-only at {path}.
Disk fullWrite failed: no space left on device.

4. edit — the highest-stakes tool

{
  "name":"edit",
  "description":"Replace an exact string in a file. The old_string must match the file content exactly, including whitespace and indentation. Include enough surrounding context to make the match unique.",
  "input_schema":{
    "type":"object",
    "properties":{
      "path":       {"type":"string","description":"Absolute path."},
      "old_string": {"type":"string","description":"Exact text to replace, including indentation. Must be unique in the file unless replace_all is true."},
      "new_string": {"type":"string","description":"Replacement text. Must differ from old_string."},
      "replace_all":{"type":"boolean","description":"Replace every occurrence. Default false."}
    },
    "required":["path","old_string","new_string"]
  }
}

Why exact-match and not line numbers or diffs: line numbers drift the moment anything above changes, and models produce malformed unified diffs at a meaningful rate. Exact string matching fails loudly and safely — a non-match changes nothing, and the error tells the model precisely what to fix. This is the design decision that makes reliable editing possible.

Semantics

Errors — these matter more than the happy path

ConditionMessage
No matchold_string not found in {path}.\nThe file may have changed, or whitespace/indentation may differ.\nNearest partial match at line {n}:\n{context}\nRe-read the file and copy the exact text.
Multiple matchesold_string appears {n} times in {path} (lines {list}).\nAdd surrounding context to make it unique, or set replace_all: true.
No prior readRefusing to edit {path} — not read this session. Call read({path}) first.
Identical stringsold_string and new_string are identical — this edit would do nothing.
File changed since read{path} changed on disk since you read it. Re-read before editing.

The "nearest partial match" in the no-match error is worth the implementation cost: it turns a dead-end into a recoverable turn. Compute it with a similarity pass over candidate windows.


5. glob

{
  "name":"glob",
  "description":"Find files matching a glob pattern. Returns paths sorted by modification time, newest first.",
  "input_schema":{
    "type":"object",
    "properties":{
      "pattern":{"type":"string","description":"Glob, e.g. **/*.ts or src/**/*.test.js"},
      "path":   {"type":"string","description":"Directory to search from. Defaults to workspace root."}
    },
    "required":["pattern"]
  }
}

Semantics


6. grep

{
  "name":"grep",
  "description":"Search file contents with a regular expression. This is the primary code-navigation tool — prefer it over reading files speculatively.",
  "input_schema":{
    "type":"object",
    "properties":{
      "pattern":     {"type":"string","description":"Regular expression (RE2 syntax)."},
      "path":        {"type":"string","description":"File or directory to search. Defaults to workspace root."},
      "glob":        {"type":"string","description":"Filter files by glob, e.g. *.go"},
      "output_mode": {"type":"string","enum":["content","files_with_matches","count"],"description":"Default files_with_matches."},
      "context":     {"type":"integer","description":"Lines of context around each match (content mode only)."},
      "case_insensitive":{"type":"boolean"},
      "multiline":   {"type":"boolean","description":"Allow . to match newlines."}
    },
    "required":["pattern"]
  }
}

Semantics

Errors

ConditionMessage
Invalid regexInvalid pattern: {err}. Abhed uses RE2 syntax — lookahead/backreference are unsupported. Rewrite without {construct}.
No matchesNo matches for {pattern}{in path}. Try a broader pattern or check the path.

7. bash

{
  "name":"bash",
  "description":"Run a shell command in the session sandbox. Use for builds, tests, git, and package managers. Prefer read/glob/grep for file inspection — they are cheaper and safer.",
  "input_schema":{
    "type":"object",
    "properties":{
      "command":    {"type":"string"},
      "description":{"type":"string","description":"Short human-readable description shown in the approval prompt."},
      "timeout_ms": {"type":"integer","description":"Default 120000, max 600000."},
      "background": {"type":"boolean","description":"Run detached; returns a handle. Use for servers and long builds."}
    },
    "required":["command","description"]
  }
}

Semantics

Approval — per-command scoped, not per-tool (P7). bash(npm test) allowlisted does not allowlist bash(rm -rf /). Destructive patterns (rm -rf, dd, mkfs, :(){ :|:& };:, force-push) require confirmation in every mode, including the most permissive.


8. task — subagent spawn

{
  "name":"task",
  "description":"Spawn a subagent with a fresh context to handle a self-contained subtask. Use when a task requires extensive exploration whose intermediate detail you do not need. The subagent returns only a summary.",
  "input_schema":{
    "type":"object",
    "properties":{
      "prompt":     {"type":"string","description":"Complete, self-contained task. The subagent sees none of this conversation."},
      "description":{"type":"string","description":"3-5 word label."},
      "agent_type": {"type":"string","description":"Which subagent profile: explore | test | review | general."},
      "max_turns":  {"type":"integer"}
    },
    "required":["prompt","description"]
  }
}

Semantics (per P3, arch §3)


9. plan and todo

Lightweight, but they carry real weight: they are the model's externalized working memory, and they survive compaction when the conversation doesn't (P4).

Both are cheap to call and should be called often on multi-step work. The system prompt should say so explicitly.


10. Conformance suite

Every tool ships with tests that a new model must pass before serving traffic (arch §5, capability probe). These are the tests that catch adapter bugs before users do:

TestAsserts
Schema round tripModel emits valid args for every tool
Exact-match editWhitespace-sensitive replacement succeeds
Multi-match rejectionAmbiguous edit errors rather than guessing
Read-before-editUnread-file edit is refused
Error recoveryModel recovers from each documented error within 2 turns
Truncation handlingModel uses offset/limit after a truncation notice
Non-zero exitModel reasons about a failing test rather than retrying blindly
Path disciplineRelative paths corrected, escapes refused
Budget exhaustionSubagent cap produces clean failure, not a hang

The error-recovery test is the important one. It measures whether your error messages actually teach — which is the difference between a tool set that works and one that frustrates the model into loops.