Skip to content

The file tree is an API

A generated app grows to 600 lines across HTML, CSS, and JavaScript. The user asks to change one validation message. A naive agent reads all three files, rewrites one of them in full, runs a check, and then reads it again to confirm the edit. The change is tiny. The context cost is not.

Once an agent works on durable files, the file tree becomes an API. Its operations need the same care as any other API: bounded responses, concurrency checks, predictable errors, and methods shaped around common tasks.

This follows directly from Apps as conversation artifacts. A durable app is useful only if later turns can inspect and edit it cheaply.

Compact concept map

text
discover -> locate -> inspect -> mutate -> verify
   ls       grep      read      edit      diff
                              write
                              remove

Each operation should answer one question. Combining discovery, full reads, edits, and publishing into one loose command makes failures hard to resume.

Working vocabulary

Manifest is the list of paths plus compact metadata such as bytes, lines, and file type.

Range read returns numbered lines from one file.

Outline is a deterministic list of structural landmarks such as headings, selectors, functions, classes, and element IDs.

Exact edit replaces one uniquely matching string.

Line edit replaces a numbered range while checking the revision the agent read.

Revision guard rejects a mutation when the draft changed after the agent inspected it.

Reserved file is generated or managed by the platform and cannot be edited through normal app tools.

Start with cheap discovery

An ls operation should return paths, byte counts, line counts, and revision state. That is enough to choose the next call.

json
{
  "revision": 17,
  "publishedRevision": 16,
  "files": [
    {"path": "index.html", "lines": 78, "bytes": 2410},
    {"path": "app.js", "lines": 412, "bytes": 13820},
    {"path": "styles.css", "lines": 205, "bytes": 5170}
  ]
}

Do not return contents from ls. The point is to make later reads deliberate. A search operation can then locate a label, selector, function, or import and return a few context lines around each match. Cap the number of hits. Search results that fill the context window defeat the method.

Read small files whole. For large files, require a range or return a short head plus an outline. A hard error that says "file too large" forces another call without teaching the model where to read. An outline gives it useful anchors in the same response.

Number every returned line. Human and model readers both reason better about a stable location than an anonymous text block.

Offer more than one edit shape

Exact string replacement is a good default:

json
{
  "path": "app.js",
  "old": "status.textContent = 'Invalid';",
  "new": "status.textContent = 'Enter an amount above zero';"
}

Require old to match exactly once. Zero matches means the agent read stale or incorrect text. Multiple matches means the edit is ambiguous. Both should fail without changing the file.

Exact replacement becomes awkward for nested templates, repeated markup, or generated data. Add a revision-guarded line replacement:

json
{
  "path": "app.js",
  "from": 118,
  "to": 124,
  "expectedRevision": 17,
  "content": "..."
}

The revision guard matters. Line numbers drift after any prior edit. If the revision changed, reject the operation and return the current revision. Never apply an old range to new text.

Whole-file write still has a place. It is the right operation for a new file, a small file, or a deliberate rewrite. Append is useful for data chunks and changelogs. Remove should refuse reserved platform files.

The tool set is not about purity. It is about matching the mutation to the amount of knowledge the agent has.

Keep content out of operational logs

Tool calls often become visible in logs, synced operation rows, traces, or model history. Store compact input and output:

text
app_edit app.js, 1 replacement, r17 -> r18
app_read app.js, lines 100-145, r17

Do not copy full file bodies into the operation ledger. The app store already holds them. Duplicate storage expands replicas, raises privacy risk, and makes compacted model transcripts larger.

Error messages should remain useful. Return the path, expected and actual revision, match count, or nearest legal file. Do not return the entire source as "context" for a failed edit.

Generated files need a boundary

Some files come from platform configuration: service workers, manifests, icons, privacy pages, metadata blocks, or release assets. Mark them as generated. Normal edit tools should refuse changes and name the source configuration that controls them.

This prevents a common drift pattern. The agent patches a generated service worker. The next check regenerates it and silently removes the patch. A clear refusal is cheaper than letting that happen.

Reserve documentation files too when the platform uses them as machine inputs. If an app manifest drives launch output, edits may be allowed, but the parser and ownership rules must be explicit.

Choices and discarded options

Use exact replacement first. Unified diffs are expressive, but models often produce malformed hunks or stale context. Exact replacement has a simple success rule. Line replacement covers the cases where exact matching becomes brittle.

Return outlines for large files. Refusing whole reads saves tokens but adds blind retries. Returning the first section plus structural landmarks preserves the bound and points to useful ranges.

Keep separate edit and publish operations. Editing should be local and cheap. A provider deployment after every mutation burns time and quota. A later high-level ship operation can combine the final stages.

Use draft revisions, not file modification times. Timestamps collide, differ across systems, and do not identify a coherent multi-file state. One monotonically increasing draft revision is easier to guard.

Avoid a virtual shell. Shell semantics add quoting, globbing, working directories, and destructive commands. A narrow file API is easier to validate and log.

Failure patterns to test

Repeated whole-app reads. Track read calls and returned lines per turn. High numbers usually mean the context lacks a manifest, outline, or previous-change summary.

Ambiguous string edits. Return the match count. Do not choose the first occurrence.

Stale line ranges. Require the revision from the earlier read.

Path traversal and hidden files. Normalize paths, reject absolute paths and parent segments, and restrict reserved names.

Binary files rendered as text. Return encoding and decoded size. Do not place binary contents in model context.

Duplicate paths after normalization. Reject them before saving.

A check regenerates files after the model edits them. Mark generated paths and refuse direct mutation.

A no-op edit bumps the revision. Compare bytes first. Idempotent writes keep caches useful.

Operator's checklist

  • Does ls omit file bodies?
  • Can search locate code with bounded context?
  • Do large reads return an outline instead of a dead end?
  • Does every mutation return the new revision?
  • Are range edits guarded by the revision that supplied their line numbers?
  • Do exact edits require one match?
  • Are paths normalized before validation and storage?
  • Are generated files marked and protected?
  • Do operation logs exclude source content?
  • Are identical writes no-ops?
  • Can the prompt include the previous turn's changed ranges?
  • Are read volume and edit failures measured?

A good file API reduces the amount of source an agent must remember. It also makes history possible because every successful mutation has a clear revision boundary. Revisions, diffs, and undo builds that safety net.

Built from field notes on durable software systems.