HN 日本語サマリー

← 一覧へ戻る
Web開発

ツールではなくファイル:仮想ファイルシステムとbashでエージェントを構築した方法

Files over tools: how we built our agent with a virtual filesystem and bash (knock.app)

9 pointsby cjbell1 コメント

要約

Knockは、顧客メッセージングリソースを管理するAIエージェント「Knock Agent」を構築しました。このエージェントは、仮想ファイルシステムとbashを活用して、APIツールに依存する従来のアプローチよりもスケーラブルな設計を実現しています。ファイルシステムを通じてコンテキストを取得し、bashスクリプトで操作を行うことで、より柔軟で効率的なエージェント開発が可能になりました。このアプローチは、既存のCLIツールとの親和性も高く、Elixirエコシステム内で開発されました。

全文翻訳

Files over tools: how we built the Knock Agent using a virtual file system and bashLast updated:09 Jul 2026|12 min read|Summarize:ChatGPTClaudePerplexityGoogle GeminiGitHub CopilotGrokIn March 2026, we shipped the Knock Agent: an AI agent for managing all your customer messaging resources in Knock. It can create and update workflows, templates, and audiences, help segment users, and answer questions about how your customer messaging is performing. The agent can be invoked from the Knock dashboard, a connected Slack workspace, the API, or our MCP server. In this post, we'll take a look behind the scenes at how we architected and built our agent using bash, a virtual file system, and our management API. Defining the vision for our agentOur vision was to create an agent that could manage everything that can be accessed from the Knock dashboard. We wanted the agent to create messaging that uses your company's design system, matches the tone of voice, and understands how your data is modeled. To bring this vision to life, we needed to create a rich agent harness that could access the full breadth of data in your Knock account and use that context while creating your messaging and answering any questions. We built our first prototype to operate exclusively on workflows, which is a deep domain with plenty of nuance around how it works. We used a tool-per-type pattern, where each tool exposed a primitive from our management API, such as adding a delay step to a workflow, or building an email template using our visual blocks language. The tool description encoded the idiosyncrasies of working with that tool and gave examples, while the tool input described the field types and what was required. While this worked and led to good results, we realized that this approach would not scale: we'd be exposing a tool per resource in the management API, which would bloat the context window of the agent without us implementing a more sophisticated tool routing layer or introducing a scripting language for the agent to use. Fewer tools, more filesWe found a blog from the team at Vercel, "How to build agents with filesystems and bash", where they describe building an agent for d0, their internal data tool. They explain how the filesystem is a great way for an agent to discover the context it needs, and how you can map a domain to a filesystem to power this context layer. At Knock, we've long had a CLI which enables teams to pull down their Knock resources, such as messaging templates and components, and operate on them locally via the filesystem. The idea here was simple: instead of exposing tools that mirror our management API, we give the agent a filesystem with the contents of your account, and the ability to use bash to script on top of that filesystem. That way, the agent can explore the filesystem to gather context, using bash to efficiently write scripts as needed. When the agent wants to make an edit to a workflow or template, it modifies a file in place in the filesystem and calls back to Knock to persist that change. This pattern is much closer to Claude Code and other coding agents than other in-product assistants, and it fits the philosophy we've already established with our CLI and local-first approach to maintaining your customer messaging. A bash for agents, in ElixirOver the 2025 holidays, Malte from Vercel released just-bash. The idea was simple: let agents have a unix filesystem and a bash environment, without booting a Linux image. In practice, that meant creating a virtual bash implementation in TypeScript with a virtual filesystem that an agent could call. Knock is a company firmly rooted in the Elixir ecosystem. We wanted a virtualized bash interpreter of our own. Our options were to 1) forgo the work we had done on our agent and rebuild it in TypeScript, or 2) port a version of the library to Elixir. Fortunately, a library like just-bash is a reproducible target for an agent. It has a well-defined surface area and a thorough test suite covering the commands it supports (jq, ls, cat, etc.) — fixtures our Elixir port reuses verbatim. In one of the all-time nerd snipes, we convinced agent-pilled friend of Knock, Ivar Vong, to give it a go. Many tokens later, we had a complete bash interpreter and virtual filesystem in Elixir, which he also called just-bash, with an extensive test suite and a robust security model. Why not a sandbox?The obvious question: why not give the agent a full computer via a sandbox rather than a virtual filesystem? After all, there are a number of vendors we could have picked to power it. We believe a full sandbox is—at this stage—overkill for our simple needs. Starting a sandbox comes at a performance cost compared to the virtual, in-memory option, and introduces harder synchronization problems for content and changes made outside our app which we wanted to punt on for now. We will revisit this decision should the complexity scale tip. Adding a full scripting language (like Python) outside of bash for the agent to write might be the deciding factor. We follow the principle of decoupling the hands from the brain, so moving to a real sandbox should just mean swapping calls to the virtual one for the real one. Wiring it togetherArmed with just-bash in Elixir and all of the building blocks, we assembled our filesystem-backed agent. We run a typical agent loop on top of the Anthropic API directly, using different models for the task at hand. That agent loop is backed by a durable workflow that can safely retry steps, using the excellent Oban library in Elixir that backs its state with Postgres. Within an agent session, we attach a sandbox which includes the just-bash instance and the full virtual filesystem. A sandbox for us is just another process that runs in our Elixir cluster. Sandboxes are started lazily whenever a request first needs to use the filesystem. This ensures that requests that don't need the filesystem return quick answers with a minimal footprint. When we don't have an existing sandbox process running, the filesystem is bootstrapped with the metadata of the objects in the account. This ensures that the agent has a full catalog of resources that it needs, but that we can lazily load the full, larger resources before the agent needs to operate on them. Here's what that filesystem looks like: |- account.json # account context |- channels.json # the set of channels available |- workflows.json # the full list of workflows on the account |- ... # lots of other resources When the agent wants to explore specific workflows or templates, it can request that they be loaded into the filesystem in bulk by calling the load_resources tool. workflows/ some-workflow/ workflow.json # the workflow configuration (steps, settings etc) steps/ email_1/ template.html # the message template for the email step in_app_1/ template.md # the message template for the in-app step The sandbox process is long-lived, so subsequent turns will have this content loaded and ready to operate on. This is perfect for multi-turn conversations where you go back and forth with the agent to make a series of updates to a workflow or message template. We expose a few other tools for the agent to work with: bash – executes bash commands. read_file – reads a file from the filesystem, preferred over cat. edit_file – makes a targeted edit to a file (replace string x with y). write_file – writes an entire file to the filesystem. upsert_resource – persists changes made on a resource in the filesystem back to Knock. The only tool of note here outside of file operations is the upsert_resource tool. It persists filesystem changes back to the Knock account by reading a bundle stored on the virtual filesystem and calling the same internal methods as our management API. Our agent loop emits a full event log of the prompt it received, actions it performed, and the response it sent. This event log is buffered in memory and written asynchronously to a Postgres store as t