MultiEdit Tool

Apply many find-and-replace edits to a single file atomically and lock-safely

The multiedit tool applies many find-and-replace edits to one file in a single atomic, lock-safe operation.

python
from praisonaiagents import Agent
from praisonai.tools.multiedit import multiedit

agent = Agent(
    name="editor",
    instructions="Apply the requested edits to files.",
    tools=[multiedit],
)

agent.start("Rename `foo` to `bar` in src/app.py and fix the typo on line 12.")
Diagram

Safe multi-edit

📝 edits + filepath

🔒 fcntl.flock LOCK_EX

📖 read snapshot

🔁 apply all edits

📄 write tempfile

💾 fsync

♻️ os.replace (atomic)

🔓 release lock

Quick Start

  1. Simple Usage

    Apply several edits to one file in a single call:

    python
    from praisonai.tools.multiedit import multiedit
    
    result = multiedit(
        filepath="src/app.py",
        edits=[
            {"old": "def foo", "new": "def bar"},
            {"old": "return foo(x)", "new": "return bar(x)"},
        ],
    )
    print(result["success"], result["edits_applied"], result["diff"])
  2. Dry Run

    Preview the diff without touching disk or taking the lock:

    python
    from praisonai.tools.multiedit import multiedit
    
    result = multiedit(
        filepath="src/app.py",
        edits=[{"old": "def foo", "new": "def bar"}],
        dry_run=True,   # prints the diff, writes nothing, does not take the lock
    )
  3. Inside an Agent

    Give the tool to an agent so it can edit files on request:

    python
    from praisonaiagents import Agent
    from praisonai.tools.multiedit import multiedit
    
    agent = Agent(
        name="editor",
        instructions="Apply the requested edits precisely.",
        tools=[multiedit],
    )
    agent.start("Fix the typo `recieve` → `receive` in src/app.py")

How It Works

Diagram
FilesystemMultiEditAgentUserFilesystemMultiEditAgentUserAsk to edit a filemultiedit(filepath, edits)flock LOCK_EX on <filepath>.lockread filepathapply edits in memorywrite tempfile + fsyncos.replace(tmp, filepath)release lock{success, edits_applied, diff}Summary

The tool locks the file, reads a snapshot, applies every edit in memory, then writes the result to a temp file, fsyncs it, and atomically replaces the original.


Safety Guarantees

GuaranteeHow it is deliveredWhat it prevents
Atomic writeTemp file (.tmp_<hex>, same directory) → write → flush → os.fsync → os.replace, via the shared atomic_write() helper in praisonai.code.utils.file_utils — also used by code_write_file, code_search_replace, code_apply_diff since PraisonAI PR #6205A cancellation / SIGTERM / disk-full mid-write can no longer leave the file zero-byte or partial — the original file stays intact until os.replace swaps it
Durableos.fsync(f.fileno()) before renamePower loss no longer leaves an empty file after apparent success
Mode preservedTemp file is created with the target’s permission bits, then os.chmod restores them before replaceA 0o755 script does not become 0o600; a 0o600 file is never world-readable mid-write
Ownership preserved (best-effort)os.chown(tmp, uid, gid) (needs privilege; no-op on Windows)A group-owned file editing on a shared filesystem does not silently change owner
No lost updates between concurrent agentsAdvisory fcntl.flock(LOCK_EX) on <filepath>.lock around the full read-modify-writeTwo overlapping multiedit() calls on the same file can no longer both read the same snapshot and have the later write discard the earlier edit
Portablefcntl is imported inside _file_lock; unavailable-import (Windows) yields a no-op contextTool still works on Windows; only the cross-process lock is unavailable there
Dry-run bypassdry_run=True skips both the lock (uses contextlib.nullcontext()) and the writeCheap previews do not contend for a lock or touch disk

Configuration Options

OptionTypeDefaultDescription
filepathstr(required)File to edit. Must resolve safely inside PRAISONAI_WORKSPACE (see File Tool Workspace Confinement); protected paths are refused (see Protected Paths)
editsList[Dict[str, Any]](required)Ordered list of edits. Each entry is {"old": str, "new": str, "line"?: int, "fuzzy"?: bool}
dry_runboolFalseIf True, generate the diff but do not lock or write
workspace_rootOptional[str]NoneOverrides PRAISONAI_WORKSPACE for this call

Return dict:

KeyTypeMeaning
successboolTrue iff edits_failed == 0
edits_appliedintEdits that matched and were applied
edits_failedintEdits whose old did not match
diffstrUnified diff (difflib.unified_diff) between original and new content
errorOptional[str]Populated if an exception was raised during the read/apply/write

Common Patterns

Batch a rename across a file

python
from praisonai.tools.multiedit import multiedit

result = multiedit(
    filepath="src/service.py",
    edits=[
        {"old": "class OldName", "new": "class NewName"},
        {"old": "OldName()", "new": "NewName()"},
    ],
)

Target a specific line

python
from praisonai.tools.multiedit import multiedit

result = multiedit(
    filepath="config.py",
    edits=[{"old": "DEBUG = True", "new": "DEBUG = False", "line": 5}],
)

Best Practices

Batch related edits into one call

The file is read, locked, and written once; N sequential single-edit calls cost N locks + N fsyncs.

Use dry_run=True for previews

No lock is taken, no write happens, but you still get the diff.

Keep old distinctive

The first exact match wins; if the same substring appears multiple times, include enough surrounding context to disambiguate (or pass line as a hint).

Windows: cross-process safety is best-effort

fcntl is unavailable on Windows, so the lock context is a no-op there. Do not run multiple concurrent multiedit() calls on the same file on Windows.


Related