9. Minimum Edits to Repair a Malformed Tool Call
Problem
A model occasionally emits a tool name that's close to, but not exactly, one the runtime recognizes — a typo, a missing character, a transposed pair.
Rather than reject the call outright, the runtime wants to find how many single-character edits (insertions, deletions, substitutions) it would take to turn the emitted tool name into a known valid one, so it can offer the closest match instead of failing the call.
Source: src/9_min_edits_repair_tool_call.py
def min_edit_distance(source: str, target: str) -> int:
...
Examples:
>>> min_edit_distance("serch_web", "search_web")
1
>>> min_edit_distance("kitten", "sitting")
3
Step-by-Step Approach
- Recognize this as classic Levenshtein edit distance between two strings.
- Build a DP table dp[i][j] = edit distance between source[:i] and target[:j], sized (n+1) x (m+1).
- Initialize the base cases: dp[i][0] = i (delete all of source[:i]) and dp[0][j] = j (insert all of target[:j]).
- Fill the table row by row: if source[i-1] == target[j-1], no edit is needed at this position, so dp[i][j] = dp[i-1][j-1]; otherwise take 1 plus the minimum of the delete/insert/substitute neighbors (dp[i-1][j], dp[i][j-1], dp[i-1][j-1]).
- Return dp[n][m], the distance between the full source and full target strings.
The key insight is that every edit operation corresponds to a specific move through the table (delete = move down, insert = move right, substitute = move diagonally), so the three-way min at each cell is really just asking "which single edit gets me here cheapest."
Reference solution
def min_edit_distance(source: str, target: str) -> int:
n, m = len(source), len(target)
# dp[i][j] = edit distance between source[:i] and target[:j]
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(n + 1):
dp[i][0] = i # delete all of source[:i]
for j in range(m + 1):
dp[0][j] = j # insert all of target[:j]
for i in range(1, n + 1):
for j in range(1, m + 1):
if source[i - 1] == target[j - 1]:
dp[i][j] = dp[i - 1][j - 1] # chars match, no edit needed
else:
dp[i][j] = 1 + min(
dp[i - 1][j], # delete source[i-1]
dp[i][j - 1], # insert target[j-1]
dp[i - 1][j - 1], # substitute source[i-1] -> target[j-1]
)
return dp[n][m]
Key Functions & Tricks
dp[i][0] = i / dp[0][j] = j— base cases: transforming to/from an empty string costs exactly the string's lengthdp[i][j] = dp[i-1][j-1]— matching characters carry the diagonal cost forward with no penalty1 + min(delete, insert, substitute)— the three-way recurrence covering every possible single edit at this position(n+1) x (m+1) table— sized to include the empty-prefix base cases at row/column 0
How to Recognize This Pattern
The signal is "minimum number of insert/delete/substitute operations to turn one sequence into another" — distinct from LCS (which only allows deletions from both sides, no substitution) and from longest-common-substring (which requires contiguity). The 2D DP table with the three-way min recurrence is the standard, always-correct approach. Common variations assign different costs to insert/delete/substitute instead of uniform cost 1, restrict the allowed operations (e.g. no substitution, only insert/delete — which reduces to an LCS-based formula), or ask only for the DP table's final value without reconstructing the actual edit sequence. A common pitfall is off-by-one errors in the base-case initialization (dp[i][0] and dp[0][j]), which silently corrupts every value that depends on them since the recurrence builds outward from those edges.