😸

bolt.newのメッセージングプロトコルを調べた

2024/10/22に公開

https://bolt.new/

bolt.newはClaude 3.5 SonnetVercel AI SDKで叩いて結果をWebContainersにデプロイしてプレビューする。

WebContainers以降はstaticblitzのサーバーに接続してiframeを開いてWasmをダウンロードしてきていろいろロードしているがブラックボックスなのであまり手の入れどころがない。

しかし、コード生成部分はClaudeとの入出力なので真似すれば「生成→dockerで実行する」自作のコード生成ツールが作れるのではないかと思い調べた。

処理を追う場合はブラウザのチャット入力を受け付ける POST /api/chatを起点にするといい。Remixアプリになっている

https://github.com/stackblitz/bolt.new/blob/cecbc55380313004d4abcc1105b73d52255cbe46/app/routes/api.chat.ts

Claudeとの通信時に以下のようなシステムプロンプトがついてくる。これがメッセージングのプロトコル定義になっていて、独自のXMLでファイル更新手続きの命令を生成していることがわかる。

https://github.com/stackblitz/bolt.new/blob/cecbc55380313004d4abcc1105b73d52255cbe46/app/lib/.server/llm/prompts.ts

試しにチャットでExpressでHello Worldを書いてとお願いすると以下のようなレスポンスが返ってきた

Certainly! I'll create a simple "Hello World" Express API server for you.

<boltArtifact id="express-hello-world" title="Express Hello World API Server">
<boltAction type="file" filePath="package.json">
{
  "name": "express-hello-world",
  "version": "1.0.0",
  "description": "A simple Express Hello World API server",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.17.1"
  }
}
</boltAction>

<boltAction type="shell">
npm install
</boltAction>

<boltAction type="file" filePath="server.js">
const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.json({ message: 'Hello, World!' });
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
</boltAction>

<boltAction type="shell">
npm start
</boltAction>
</boltArtifact>

The Express "Hello World" API server is now set up and running. You can test it by sending a GET request to the root endpoint.
  1. <boltAction type="file" filePath="package.json">でpackage.jsonを書き込んで保存
  2. <boltAction type="shell">でnpm installをコンテナで実行
  3. <boltAction type="file" filePath="server.js">でserver.jsを保存
  4. <boltAction type="shell">でnpm startをコンテナで実行

これが基本動作になる。

ユーザーがさらに発言をすると、上記の結果も履歴として送信する。

ファイルの内容は内部的にはnanostoresにkey-valuesで保持されていて、ユーザーが更新したらdiffを生成してそれもClaudeに送信する。

たとえば生成されたファイルをエディタ上で書き換えるとそれもClaudeに伝えるために以下のようにdiffつきで送信する。ここではapp.post('/loginという行を中途半端に追記してadd login actionと言ってみた

  {
    role: 'user',
    content: '<bolt_file_modifications>\n' +
      '<diff path="/home/project/server.js">\n' +
      '@@ -5,7 +5,9 @@\n' +
      " app.get('/', (req, res) => {\n" +
      "   res.json({ message: 'Hello, World!' });\n" +
      ' });\n' +
      ' \n' +
      "+app.post('/login'\n" +
      '+\n' +
      ' app.listen(port, () => {\n' +
      '   console.log(`Server running at http://localhost:${port}`);\n' +
      ' });\n' +
      '\n' +
      '</diff>\n' +
      '</bolt_file_modifications>\n' +
      '\n' +
      'add login action'
  }
]

bolt_file_modificationsというパッチのタグが使われていることが分かる。これはfileModificationsToHTML で実装されている。

https://github.com/stackblitz/bolt.new/blob/cecbc55380313004d4abcc1105b73d52255cbe46/app/utils/diff.ts#L92-L108

これに対するレスポンス

Certainly! I'll add a login action to your Express server.

<boltArtifact id="express-hello-world-login" title="Express Hello World API Server with Login">
<boltAction type="file" filePath="server.js">
const express = require('express');
const app = express();
const port = 3000;

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ message: 'Hello, World!' });
});

app.post('/login', (req, res) => {
  const { username, password } = req.body;

  // This is a simple example. In a real application, you'd validate credentials against a database.
  if (username === 'admin' && password === 'password') {
    res.json({ message: 'Login successful', user: username });
  } else {
    res.status(401).json({ message: 'Invalid credentials' });
  }
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
</boltAction>

<boltAction type="shell">
npm start
</boltAction>
</boltArtifact>

The login action has been added to your Express server. You can now make POST requests to the `/login` endpoint with a JSON body containing `username` and `password`.

先ほどとおなじように<boltAction type="file" filePath="server.js">を返してくる。ここで分かるのはファイルの全内容がレスポンスには含まれるということ、更新は差分ではない。そして更新対象のファイルのみレスポンスには含まれている。


以上のようにシステムプロンプトで定義されているタグをパースするとファイルのコンテンツを取得することができる。同じプロンプトを使いパースした結果をローカルに保存して任意のコンテナでdocker runするというツールぐらいなら作れそうな気がしてきた。

またチャットのたびに履歴としてファイル内容を送信しているのでトークン数は会話数に応じて増加する。/api/enhancerというエンドポイントも別途用意されていて、プロンプトのrefineに使用されると思われる(どのタイミングで実行されるのかまだ調べてない)。

https://github.com/stackblitz/bolt.new/blob/cecbc55380313004d4abcc1105b73d52255cbe46/app/routes/api.enhancer.ts

一方ファイルの更新はレスポンスに全文を含む。こちらもファイルのサイズが増えるごとに一部の書き換えのみでもトークンは消費する。

Cursorの場合は「レスポンスに含まれるコードを省略表記にする→そこから差分を検知してapplyする」という仕組みを備えているのでトークン削減する方法はまだありそうだ。

Discussion