Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 96 additions & 85 deletions apps/website/content/docs/deep-agents/capabilities/filesystem.mdx
Original file line number Diff line number Diff line change
@@ -1,111 +1,122 @@
---
title: Filesystem
description: StateBackend keeps a Deep Agents workspace on the graph state, and a FilesystemPermission in interrupt mode routes writes through the chat interrupt panel.
description: How the Deep Agents filesystem example keeps the agent workspace on graph state and pauses writes under /reports/ for human approval
---

# Filesystem

`create_deep_agent` always installs `FilesystemMiddleware`, so a Deep Agents agent always has `ls`, `read_file`, `write_file`, and `edit_file`. What decides whether a user interface can render that workspace is not the middleware. It is the backend.

```python
from deepagents import create_deep_agent
from deepagents.backends import StateBackend
from deepagents.middleware import FilesystemPermission

graph = create_deep_agent(
model=ChatOpenAI(model="gpt-4.1", temperature=0),
tools=[lookup_field_elevation, lookup_runway_length],
system_prompt=(PROMPTS_DIR / "filesystem.md").read_text(),
backend=StateBackend(),
permissions=[
FilesystemPermission(operations=["write"], paths=["/reports/**"], mode="interrupt"),
],
)
```
`create_deep_agent` always installs `FilesystemMiddleware`, so a Deep Agents agent always has `ls`, `read_file`, `write_file`, and `edit_file`. What decides whether a user interface can render that workspace is not the middleware. It is the backend. The running example is a dispatch filing desk that gathers airport data, keeps working notes, and files a report, and this page walks the three files behind its workspace panel and its write approval.

`StateBackend` stores the agent's files on the graph state under `files`, which means every write arrives at the client as a `values` update. A backend that writes anywhere else — a host directory, a remote object store — puts nothing on the state, and a panel bound to `files` stays empty no matter how busy the agent is. The choice of backend is the choice of whether the workspace is renderable at all.
## What the demo does

## What the demo shows
The Run tab shows the prebuilt `<chat>` composition beside a workspace panel. The welcome suggestion, "Runway note for KASE", asks the agent to work up a runway suitability note: save the raw lookups to `/notes/kase-data.md`, then write the finished note to `/reports/kase-runway.md`.

The demo is the same dispatch desk, given a task that produces artifacts: gather field data for two airports, keep working notes, then file a report.
The scratch file under `/notes/` appears in the panel the moment the agent writes it. The report does not. A write under `/reports/` pauses the run, and an approval card appears below the tree while the target path is already listed as a dimmed, italic row badged "awaiting approval".

The workspace panel renders a directory tree grouped by path. Scratch files under `/notes/` appear the moment the agent writes them, with no ceremony. A write under `/reports/`, however, stops the run.
Accept lets the write land and the run continue. Ignore rejects it, and the agent finishes without the file. The card also offers Edit and Respond, which this example leaves unhandled. Selecting any file in the tree shows its contents in the preview underneath.

That is the `FilesystemPermission` above. In `interrupt` mode a matching call pauses for human approval instead of executing, and the pause surfaces through the standard chat interrupt panel — the same component every other LangGraph interrupt uses. There is no Deep Agents specific interrupt UI, because there is no Deep Agents specific interrupt.
## How it is built

Approving the write lets the run continue and the file lands in the tree. Rejecting it returns the model to work without the file.
Three files carry the feature: a Python graph that builds the agent, an application config that registers it, and an Angular component that projects the workspace and maps the approval buttons onto resume payloads. Open the Code tab to read them in place.

<Callout type="warning" title="Anchor the permission pattern">
Give the pattern a literal prefix, as `/reports/**` does. Bulk tools such as `ls`, `glob`, and `grep` decide whether to fire the permission based on whether their search subtree could overlap the anchored prefix. A fully unanchored pattern collapses to the root and fires on every listing, which turns an approval gate into an interruption on each directory read.
</Callout>
### The lookups the notes are made of

## How it reaches the UI

### The tree

`files` is a flat map from absolute path to contents. Splitting each key on its last slash is enough to group it into directories.

```ts
protected readonly files = computed<WorkspaceFile[]>(() => {
const raw = (this.agent.value() as Record<string, unknown> | undefined)?.['files'];
const entries = new Map<string, string>();
if (raw && typeof raw === 'object') {
for (const [path, contents] of Object.entries(raw as Record<string, unknown>)) {
entries.set(path, typeof contents === 'string' ? contents : JSON.stringify(contents));
}
}
return [...entries.entries()].map(([path, contents]) => {
const slash = path.lastIndexOf('/');
return {
path,
directory: slash > 0 ? path.slice(0, slash) : '/',
name: path.slice(slash + 1),
contents,
};
});
});
```
The agent needs something to write down. Two ordinary LangChain tools answer field elevation and runway length for a handful of ICAO codes, and the system prompt tells the agent to gather that data before it writes anything.

<ExampleCode file="graph.py" region="lookup-tools" title="graph.py — the lookup tools" />

Nothing about these tools is filesystem specific; they are the source of the content the agent files.

### The backend that makes the workspace renderable

`StateBackend` stores the agent's files on the graph state under `files`, so every write reaches the client as a `values` update. A backend that stores files anywhere else, such as a host directory or a remote store, puts nothing on the state, and a panel bound to `files` stays empty no matter how busy the agent is. `FilesystemPermission` is the second half: a rule over operations and path patterns, and in `interrupt` mode a matching call pauses for human approval instead of executing.

### The pending write
<ExampleCode file="graph.py" region="agent" title="graph.py — the agent" />

While an approval is open, the file does not exist yet — it is an argument on a paused tool call. Reading it off the interrupt lets the tree show the file as a ghost row, so the reviewer sees where it is about to land before deciding.
`StateBackend` comes from `deepagents.backends` and `FilesystemPermission` from `deepagents.middleware`; no interrupt wiring is needed beyond the rule, because an interrupt-mode rule auto-installs `HumanInTheLoopMiddleware`.

The interrupt payload is `{ action_requests: [{ name, args }] }`, and for `write_file` the target path is `args.file_path`:
### The agent provider

```ts
protected readonly pendingPath = computed<string | null>(() => {
for (const interrupt of this.agent.langGraphInterrupts() ?? []) {
const value = (interrupt as { value?: unknown }).value as
| { action_requests?: Array<{ args?: Record<string, unknown> }> }
| undefined;
for (const request of value?.action_requests ?? []) {
const path = request.args?.['file_path'];
if (typeof path === 'string') return path;
}
}
return null;
`provideAgent()` registers the agent once for the whole application, and `provideChat({})` registers the configuration the `<chat>` composition reads, here left at its defaults. The example passes a factory because it resolves its connection details at runtime from the host that serves the demo.

<ExampleCode file="app.config.ts" />

Your own application does not need the factory. Pass the values directly:

```typescript
provideAgent({
apiUrl: 'https://your-deployment.langgraph.app',
assistantId: 'da-filesystem',
});
```

### The resume payload
`assistantId` must match the graph name in `langgraph.json`, here `da-filesystem`.

`deepagents` expects a structured decision, not a bare string and not a bare list:
### The pending write, read off the interrupt

```ts
protected onInterruptAction(action: InterruptAction): void {
if (action === 'accept') {
void this.agent.submit({ resume: { decisions: [{ type: 'approve' }] } });
} else if (action === 'ignore') {
void this.agent.submit({ resume: { decisions: [{ type: 'reject' }] } });
}
}
```
While an approval is open the file does not exist yet. It is an argument on a paused tool call, so the only place to find it is the interrupt payload, which `injectAgent()` exposes as the `langGraphInterrupts()` Signal. The payload is `{ action_requests: [{ name, args }], review_configs: [...] }`, and for `write_file` the target path is `args.file_path`.

<ExampleCode file="filesystem.component.ts" region="pending-path" title="filesystem.component.ts — the pending path" />

Reading it lets the tree show the file before it lands, so the reviewer sees where the write is headed while deciding.

### The file map, projected into a tree

`files` is a flat map from absolute path to a file record; the text is on its `content` field, which is why the projection stringifies anything that is not already a string. `agent.value()` returns the live graph state that holds the map. The projection reads that map, adds the pending path as a ghost entry when one is open, and splits each key on its last slash to derive a directory and a name.

<ExampleCode file="filesystem.component.ts" region="files" title="filesystem.component.ts — the file projection" />

Passing a bare list raises a `TypeError` on the server rather than a validation error the browser can show, so the failure appears as a dead run rather than as a rejected submission. The shape is worth getting right the first time.
Because the panel is a projection of state rather than a replay of `write_file` calls, an edit that rewrites an existing file shows up as one changed file here and as two entries in a tool call log.

## Next steps
A second computed groups the flat list by directory, which is all the structure a tree needs.

<ExampleCode file="filesystem.component.ts" region="tree" title="filesystem.component.ts — grouping by directory" />

### The panel

The sidebar renders the grouped tree, a preview of the selected file, and the interrupt panel. A pending row carries `data-pending`, which dims and italicizes it; the badge is rendered from the same `pending` flag. `<chat-interrupt-panel>` is the same component every other LangGraph interrupt uses. There is no Deep Agents specific approval component, because there is no Deep Agents specific interrupt.

<ExampleCode file="filesystem.component.ts" region="workspace-panel" title="filesystem.component.ts — the workspace panel" />

Keeping the tree and the approval in one sidebar is the point of the layout: the reviewer reads the destination and the decision in the same glance.

### Resuming with a decision

`<chat-interrupt-panel>` emits an `InterruptAction` of `accept`, `edit`, `respond`, or `ignore`, and the component turns the two it handles into resume payloads. `HumanInTheLoopMiddleware` resumes on an object with a `decisions` list, one decision per paused tool call, each `{ "type": "approve" }`, `{ "type": "edit" }`, or `{ "type": "reject" }`.

<ExampleCode file="filesystem.component.ts" region="resume" title="filesystem.component.ts — resuming the run" />

The demo always sends exactly one decision, which is enough because only one write is ever paused here. The middleware rejects a resume whose decision count differs from the number of hanging tool calls, so a turn that batches two writes into one interrupt needs two decisions.

The consequence is visible in the tree. On Accept the write lands, the ghost row stops being pending, and the preview shows the real file content. On Ignore the interrupt clears without a file being written, so the row that only ever existed as a projection of the pending path disappears.

<Callout type="warning" title="The resume payload is an object, not a list">
The middleware reads `interrupt(request)["decisions"]`, so a bare list or a bare string raises a `TypeError` on the server rather than a validation error the browser can show. The failure appears as a dead run rather than as a rejected submission, so the shape is worth getting right the first time.
</Callout>

## Permission rules

A `FilesystemPermission` carries three fields: the `operations` it covers, the `paths` it matches, and the `mode` it applies. Rules are evaluated in declaration order and the first match wins; a call that matches no rule is allowed. Subagents inherit the parent rules unless they declare `permissions` of their own, which replaces the parent set entirely.

The three modes are `allow`, which lets the call proceed, `deny`, which returns a permission-denied error to the model, and `interrupt`, which pauses the call for human approval. Path patterns must start with `/` and may not contain `..`.

<Callout type="warning" title="Anchor the permission pattern">
Give the pattern a literal prefix, as `/reports/**` does. Bulk tools such as `ls`, `glob`, and `grep` decide whether to fire the permission based on whether their search subtree could overlap the anchored prefix. A fully unanchored pattern collapses to the root and fires on every listing, which turns an approval gate into an interruption on each directory read.
</Callout>

- [Planning](/docs/deep-agents/capabilities/planning) — the todo list the agent keeps while it files.
- [Skills](/docs/deep-agents/capabilities/skills) — the same backend machinery, mounted read-only.
- [Interrupts](/docs/langgraph/guides/interrupts) — the interrupt lifecycle underneath the approval.
- [Chat interrupt panel](/docs/chat/components/chat-interrupt-panel) — the component that renders the approval.
## What's Next

<CardGroup cols={2}>
<Card title="Planning" href="/docs/deep-agents/capabilities/planning">
The todo list the agent keeps while it files.
</Card>
<Card title="Skills" href="/docs/deep-agents/capabilities/skills">
The same backend machinery, mounted read-only.
</Card>
<Card title="Interrupts" href="/docs/langgraph/guides/interrupts">
The interrupt lifecycle underneath the approval.
</Card>
<Card title="Chat interrupt panel" href="/docs/chat/components/chat-interrupt-panel">
The component that renders the approval.
</Card>
</CardGroup>
Loading
Loading