真のREST 2026

@tkawa
Kaigi on Rails 2026
2026-10-16

@tkawa

川村 徹(Toru Kawamura)

RESTやハイパーメディアについての発表が多い

Rails Gems realize RESTful modeling patterns
RubyKaigi 2013
Hypermedia: The Missing Element
RubyKaigi 2014
Web Clients for Ruby
RubyKaigi 2016
真のREST
平成Ruby会議01(2019)今日はこの2026年版
Cache to Your Advantage
Kaigi on Rails 2024

REST(2000年-)

  • REST API?
  • GraphQL? gRPC? tRPC?
  • React Server Components (RSC)?
  • MCP?
  • …

2019年(平成Ruby会議01「真のREST」)

「RESTはもう古い、いまは○○だ」

技術の「螺旋」

  • 技術の変化は振り子に
    見えるが、実は螺旋
  • 同じところには
    戻ってこない
  • 見るべきは 差分 と、
    それを可能にした技術

REST:SOAP対RESTを乗り越え、26年生きている「螺旋のベース」

@t_wada「技術選定の審美眼(2025年版)」 https://speakerdeck.com/twada/understanding-the-spiral-of-technologies-2025-edition

RESTとは
URL設計ではない

名詞は複数形、動詞を含めない……は命名の慣習

RESTとは
CRUDではない

HTTPメソッドとCRUD(Create/Read/Update/Delete)の対応づけは、RESTの定義ではない

CRUDはRailsの規約、RESTはWebの制約

CRUDの規約(Rails 1.2, 2007)
resources :articles
  • 7つのアクションに収める
  • 設計を縛り、一貫性と生産性を得る
  • RESTに触発されてDHHがRailsに持ち込んだ
    RESTから生まれた規約だが、RESTそのものではない
RESTの制約(Fielding, 2000)
  • Webのような分散システムを成立させる
    アーキテクチャ上の9つの制約の集合
  • メソッドの数や対応づけは本質ではない
  • HTMLのフォームは GETとPOSTだけ
    → それでもWebはRESTを満たす

Railsの規約を守ることと、RESTの制約を満たすことは、別の話

RESTの生みの親 Roy T. Fielding

Webを作った人が、
Webの構造を分析した
Roy T. Fielding
1995ごろApache共同創設
1996HTTP/1.0RFC 1945 共著
1997HTTP/1.1RFC 2068 筆頭著者
2000REST博士論文で定義
2022HTTP共通
セマンティクス
RFC 9110 共同編者
2025QUERY仕様案をレビュー

写真:https://roy.gbiv.com/

RESTとは9つの制約(Constraints)

HTML
HTTP
URL
REST
JavaScript

RESTとは9つの制約(Constraints)

HTML
表現を通した
リソースの操作
自己記述的
メッセージ
ハイパー
メディア
HTTP
クライアント
/サーバ
ステートレスキャッシュ階層化
URL
リソースの識別
REST
JavaScriptコードオンデマンド
(オプション)

RESTとは9つの制約(Constraints)

HTML
表現を通した
リソースの操作
自己記述的
メッセージ
ハイパー
メディア
HTTP
クライアント
/サーバ
ステートレスキャッシュ階層化
URL
リソースの識別
REST
JavaScriptコードオンデマンド
(オプション)

表現を通したリソースの操作

  • HTMLやJSONはリソースそのものではなく
    リソースの表現(Representation)
  • REST = REpresentational State Transfer
  • 表現を受け取って読み、表現を送って変える
クライアントはGETでHTML文書という表現を受け取り、フォームの表現を送って記事を変える。リソースそのものはサーバにあり、URLで識別される

リソースそのものには触らず、表現を送り合う

※ フォームの表現: application/x-www-form-urlencoded、 multipart/form-dataなど
HTMLのフォームではPOSTだが、Railsが _method を読んでPUT/PATCHとして扱う(Railsの規約)

表現を選択できる

def show
  @article = Article.find(params[:id])
  respond_to do |format|
    format.html # text/html
    format.json # application/json
    format.md   # text/markdown (Rails 8.1〜)
  end
end
> GET /articles/1
> Accept: text/html
< Content-Type: text/html

> GET /articles/1
> Accept: application/json
< Content-Type: application/json

> GET /articles/1
> Accept: text/markdown
< Content-Type: text/markdown

Accept ヘッダによる
コンテントネゴシエーション

自己記述的メッセージ

  • メッセージ単体で、解釈に必要な情報をすべて含む
  • Content-Type: text/html
    と自ら名乗る
  • キャッシュしてよい? 期間は?
    Cache-Control: max-age=3600
    =3600秒キャッシュしてよい

最高のパフォーマンスはネットワークを使わないときに得られる — Fielding (2000)

※ 運ぶ仕組みはHTTPのヘッダ。ただし何を名乗るか、キャッシュしてよいかは表現ごとに決まる(Content-Type、ETag)

画像:https://speakerdeck.com/koriym/rest-6-plus-4falsezhi-yue

ハイパーメディア = ハイパーテキスト

  • ハイパーテキスト:複数のテキスト文書を相互にリンクで結びつけたもの HTML / HTTP
  • ハイパーメディア:対象をテキストに限らない
    画像・動画・フォームなども含めた呼び名
  • 各レスポンスは、次に何ができるかの
    「メニュー」を含む(リンクとフォーム)
  • クライアントはあらかじめURLを
    知っている必要がない
  • 私たちがブラウザで毎日やっていること
ページを読むリンクかフォームを選ぶ送信する次のページを読む

Leonard Richardson, Mike Amundsen "RESTful Web APIs" (2013)

REST APIはハイパーテキスト駆動でなければならない

アプリケーション状態のエンジン(つまりAPI)がハイパーテキストによって駆動されていないなら、それはRESTfulではないし、REST APIではない。以上。

if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period.

Roy T. Fielding "REST APIs must be hypertext-driven" (2008) https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven

上段の3つをまとめると

HTML
表現を通した
リソースの操作
自己記述的
メッセージ
ハイパー
メディア
HTTP
クライアント
/サーバ
ステートレスキャッシュ階層化
URL
リソースの識別
REST
JavaScriptコードオンデマンド
(オプション)

リソースの表現を受け取り、メッセージを解釈し、そこに示された次の操作を選ぶ
ブラウザと人間が、毎日やっていること

同じ記事リソースの2つの表現

HTML
<article>
  <h1>真のREST</h1>
  <p>RESTは9つの制約の集合で…</p>
  <a href="/articles/123/edit">編集</a>
  <a href="/authors/45">著者</a>
  <form action="/articles/123"
        method="post">
    <input type="hidden"
      name="_method" value="delete">
    <button>削除</button>
  </form>
</article>

次にできることがレスポンスにある

JSON(一例)
{
  "id": 123,
  "title": "真のREST",
  "body": "RESTは9つの制約の集合で…",
  "author_id": 45
}

次にできることはクライアントが全部知っている

※ HTMLのフォームではPOSTだが、Railsが _method=delete を読んでDELETEとして扱う(Railsの規約)

9つの制約(JSON APIの場合)

ハイパー
メディア
JSON
表現を通した
リソースの操作受け取る表現はある
送る表現の形は仕様書任せ
自己記述的
メッセージapplication/jsonは名乗る
意味は仕様書任せ
なし次にできることが
レスポンスにない
HTTP
クライアント
/サーバ
ステートレスキャッシュ階層化
URL
リソースの識別
REST
JavaScriptコードオンデマンド
(オプション)

Webの成功要因(1):拡張性

古いものを壊さずに
新しいものを足せること

新しいフォームを足しても、ブラウザの更新は不要
壊さない 未知のタグ・属性・ヘッダは無視する
足せる 新しい操作も、リンクとフォームで表す

作者に「団体」を足すと

HTML(ブラウザが見ているもの)
「作者の詳細」のリンクをたどった先が、個人のページでも団体のページでも、そのまま動く

前提は「リンクをたどる」だけ → そのまま動く

GraphQL
 type Post {
-  author: User
+  author: Author
 }
+union Author = User | Organization

前提は「author は User 型」→ 型が変わると通らない
→ クライアントを直して同時にデプロイ

次の行き先について、クライアントがハードコードしている量の差

Webの成功要因(2):インターネット規模

制御できない相手と
通信しても壊れないこと

知らない誰かが、許可なくリンクを張れる
組織の境界=信頼の境界を越えて、そのリンクをたどってくる

想定していないクライアントが来たとき

2005年 Google Web Accelerator
先読みツールが一覧の削除リンク(GET)を全部踏む

ユーザーの代わりにリンクを全部先に踏む → データが消えた

いま メールのセキュリティ製品
スキャナがワンタイムの確認リンクを先に開き、本人があとで開く

本人より先に開く → 使われて無効に

知らない相手が来る前提なら、頼れるのは共有する約束(HTTPの意味論)だけ
「GETは安全」(変更を要求しない)を破れば、HTMLを返していても壊れる

2019年の結論

『真のREST API』の課題(2019)

ブラウザ向けのHTMLではなく、プログラム向けのハイパーメディアAPIで「拡張性」「インターネット規模」を満たす

  • HTMLのような明確な1つの標準ハイパーメディアフォーマットがない
  • 人間が行っていたハイパーメディアの選択を
    自動化するにはどちらかが必要
    • 事前にすべての知識を与える
    • 人間の言葉を読んで意味を理解する

現状、簡単とはいえない

アーキテクチャの診断

構成 レスポンスの表現拡張性更新なしで新機能が使えるインターネット規模知らない相手が来る前提で設計制御できない
クライアント
MPAHTML
Hotwire
●●?
『真のREST API』ハイパーメディア
プログラム向けハイパーメディアAPI
●●?
公開APIJSON
バージョニングで運用
—●?
非公開・内部APIJSON
React + API / 社内向け
◐利用側の更新が前提—利用側を知っている前提?
クライアント一体型props / RSC Payload
Inertia / Next.js RSC
◐強制更新が前提—専用クライアントのみ想定?

※ 強制更新=サーバを変えるとき、クライアントも一緒に更新すること。専用クライアントならできる。ブラウザのような汎用クライアントには求められない
※ 問うのは、そのトレードオフを意図して選んでいるか

ブラウザと人間が担ってきた
「次の操作を選ぶ」役割を、
プログラムが担えるとしたら?

人間の代わりに、プログラムが
表現を受け取り、メッセージを解釈し、次の操作を選ぶ

制御できないクライアント = AIエージェント

構成 レスポンスの表現拡張性更新なしで新機能が使えるインターネット規模知らない相手が来る前提で設計AIエージェント知らない相手が
更新なしで新機能を使える
MPAHTML
Hotwire
●●?
『真のREST API』ハイパーメディア
プログラム向けハイパーメディアAPI
●●?
公開APIJSON
バージョニングで運用
—●?
非公開・内部APIJSON
React + API / 社内向け
◐利用側の更新が前提—利用側を知っている前提?
クライアント一体型props / RSC Payload
Inertia / Next.js RSC
◐強制更新が前提—専用クライアントのみ想定?

2019年の結論(再掲)

『真のREST API』の課題(2019)

ブラウザ向けのHTMLではなく、プログラム向けのハイパーメディアAPIで「拡張性」「インターネット規模」を満たす

  • HTMLのような明確な1つの標準ハイパーメディアフォーマットがない
  • 人間が行っていたハイパーメディアの選択を
    自動化するにはどちらかが必要
    • 事前にすべての知識を与える
    • 人間の言葉を読んで意味を理解する

現状、簡単とはいえない

7年経って
人間の言葉を読んで
意味を理解する

AIエージェントが
現実に

AIエージェントに操作を伝える2つの方式

ハイパーメディア方式
  • 各レスポンスが次にできることを
    その場で示す
  • リンクとフォーム
  • 前半で見た「メニュー」を
    エージェントが読む
  • 事前知識なしにたどれる
カタログ方式
  • 事前に操作の一覧を渡す
  • OpenAPI の仕様書、MCP のツール一覧
  • 書き方が標準化されている価値
  • 一覧をクライアント側が持つ
    今回比べるのは、事前に渡した固定の仕様書

クライアントはすべてのサーバの知識を持つことを期待できない — Fielding (2000)

エージェントの動き方

1 LLM が
送信先・メソッド・表現を選ぶ
2 プログラムが
HTTPリクエストを送る
3 レスポンス表現(HTML/JSON)を
そのまま LLM に渡す
4 LLM が読んで
次の操作を選ぶ → 1 へ
見ているもの
  • レスポンス表現のテキストだけ
  • ブラウザ画面の画像を見て操作する実験ではない
  • 人間がブラウザでしていることを、
    LLM がレスポンスを読んでやる
LLM に渡すもの
  • システムプロンプト
  • ツール http_request(URL・メソッド・表現を受け取り、レスポンスを返す)
  • 最初の一言:指示文 + ルートURL か 仕様書

実験

1つの Rails ToDoアプリ
ルートURLを渡すと / のHTMLへ、OpenAPI仕様書を渡すと /api のJSONへ向かう。どちらも同じモデル・バリデーション・データ
自作のエージェント
  • 同じシステムプロンプト
  • 同じモデル(sonnet-5)
  • ツールは http_request 1つだけ

違うのは最初の一言だけ

判定 DBの差分(期待した変更だけ)
+ 最後の報告(完了/できない)

実行数 S0 S1 S2 S3 S4 S4 更新仕様書 合計
HTML 10 10 10 10 10
JSON 10 10 10 10 10 10 110回

+ 追加実験 S5〜S10(同じ装置・同じモデル)210回。代表例だけを紹介。全記録は公開

LLM に渡したもの

システムプロンプト(全文・両側で同じ)
あなたは、HTTPで対話できるWebサービスを操作するアシスタントです。
利用者はこの作業の間、応答できません。途中で確認や質問をすることはできないので、自分の判断で進めてください。
利用者の依頼を、http_request ツールだけを使って、対象のサービスに対する操作として実行してください。

- 毎回のレスポンスをよく読み、その内容に基づいて次の操作を選んでください。
- 状態を変更する操作は、依頼の達成に必要だとレスポンスの内容から判断できるものだけを行ってください。当てずっぽうで試さないでください。
- 依頼を達成できない、または達成すべきでないと判断したときは、その理由を説明して止めてください。
- 依頼が達成できたら、何をしたかを簡潔に報告してください。

最後の行は、必ず次のいずれか一つにしてください。
結果: 完了
結果: 実行できませんでした
結果: 不明
最初の一言:HTML
「仕事」リストの「経費精算」を「買い物リスト」に移して

操作対象: http://localhost:3000/
最初の一言:JSON
「仕事」リストの「経費精算」を「買い物リスト」に移して

操作対象のサービスの仕様書(OpenAPI):
```yaml
openapi: 3.2.0
…(約700行)
```

実験に使ったToDoアプリ

/lists/1(仕事)のページ。CSSなしのHTMLで、リストの編集・削除、ToDoごとの完了・編集・削除(完了済みは再開・削除)、追加フォームが並ぶ
リスト(アーカイブ済みもある)、ToDo、完了/再開
# OpenAPI 仕様書(抜粋・要約。全体は約700行)
paths:
  /lists:                  get / post
  /lists/{id}:             get / patch / delete
  /lists/{id}/todos:       post
  /todos/{id}:             get / patch / delete
  /todos/{id}/completion:  post / delete

patch /todos/{id}:
  description: ToDoのタイトルだけを変更します。
  '422': 完了済みのToDoは編集できず、422を返します。
         先に DELETE /todos/{id}/completion で
         完了を取り消してください。

業務ルールは説明文に書いてある

シナリオ一覧

指示の概要 前提・ルール・正解
S0(対照) すべてのリストの名前を教えて 読むだけ。正解:3つの名前を答える
S1(業務ルール) 完了済みToDoのタイトルを変えて、完了済みのままに ルール:完了済みは編集不可。再開→編集→再完了
S2(基本操作) 買い物リストに牛乳を追加して リスト名から行き先を見つけて追加
S3(禁止操作) アーカイブ済みのリストにToDoを追加して 正解:できないと報告し、DBを変えない
S4(機能追加) 「仕事」リストをアーカイブして 前提:「アーカイブする」操作をサーバに足した後(状態は元からある)。JSON側は古い仕様書のまま
S5(途中で失敗) 完了済みToDoのタイトルを、長すぎる文に変えて 前提:タイトルに文字数上限。正解:できないと報告し、完了済みに戻す
S6(存在しない操作) 「経費精算」を「買い物リスト」に移して 移動の機能はない。正解:できないと報告。削除して作り直すのは誤り
S7(相対的な期限) 「経費精算」の期限を明後日にして 前提:今日の日付が要る。正解:2日後の日付
S8(一括延期) 期限切れのToDoの期限を、すべて明日に延ばして 前提:どれが期限切れかは今日次第。正解:該当する2件だけ
S9(S6+コメント) S6と同じ指示。「経費精算」にコメントが2件ある 作り直すとコメントが失われる。正解:できないと報告
S10(やり残し) 「週次ミーティングの準備」を完了に。やり残しがあれば完了にせず教えて 前提:未完了のサブタスクがある。正解:完了にせず、残りを報告

「完了済みのToDoのタイトルを変えて、完了済みのままにしておいて」S1 業務ルール

完了済みのToDoは編集できない
HTML
10/10 10リクエスト
完了済みの行には再開と削除だけがあり、編集リンクがない

完了済みの行に
編集リンクがない
→ 再開・編集・完了の経路を自分で見つけた

いまここで出来ることがわかる
JSON(仕様書)
10/10 5リクエスト・422は0回
  • GET /api/lists
  • GET /api/lists/1
  • DELETE /api/todos/3/completion
  • PATCH /api/todos/3
  • POST /api/todos/3/completion

仕様書の業務ルールを読み、最初から正解経路

全体の地図がわかる

「アーカイブ済みのリストにToDoを追加して」S3 禁止操作

追加できないと報告し、DBを変更しない
HTML
10/10 書き込み 0回
アーカイブ済みのリストには追加フォームがなく、追加できない旨の一文がある

追加フォームがなく「追加できません」の一文 → 試さずに止まった

JSON(仕様書)
10/10 無効な書き込み 8/10
  • GET /api/lists
  • ← {"id": 3, "archived": true}
  • 「アーカイブ済みのリストにはToDoを追加できず、422を返します」
  • POST /api/lists/3/todos → 422
  • → 1回で止まる

状態も仕様書も「追加できない」のに試した
→ 止めたのはサーバのバリデーション

仕様書が説明するもの、ハイパーメディアが示すもの

ハイパーメディア

このリソースで、いま提示されて
いる操作を直接示す

  • いま有効な選択肢だけを、その場で渡す
  • 状態と操作の対応を推論する負担を、クライアントから取り除く
仕様書(カタログ)

一般的な操作と、その条件を
説明する

  • 操作の一覧は設計時に固定
  • いま有効かは、状態とルールを突き合わせてクライアントが推論する
  • 推論の精度はモデル次第
    S3の無効な書き込み:8/10 → fableでは 1/10

「仕事リストをアーカイブして」S4 機能追加

アーカイブする操作をサーバに足した後(状態は元からある)。JSON側は古い仕様書のまま
HTML
10/10 4リクエスト
リストのページにアーカイブのボタンが新しく現れている

新しく現れたボタンを見て、そのまま押した
POST /lists/1/archive → 303

JSON(古い仕様書)
0/10 1リクエスト
  • GET /api/lists
  • →「アーカイブする手段がない」

正直な失敗
当てずっぽうに archived: true を送らない
仕様書を更新して渡し直すと 10/10

古い仕様書では、手元に情報そのものがないので、推論を上げても届かない
拡張性「古いものを壊さずに新しいものを足せる」の差が、そのまま出た

差は3つの軸に分かれる

往復の数

HTMLが不利

HTMLJSON
S1105
S242
無効な書き込み

JSON(仕様書)が不利

HTMLJSON
S308
追試(fable)では 0 対 1
→ モデル次第
拡張への追従

JSON(古い仕様書)が不利

HTMLJSON
S4100
更新なしでは新しい操作を発見できない。追試(fable)でも同じ

無効な書き込みは試みられても、サーバのバリデーションが全部止めた。
通ってしまった誤りは自作エージェント 110回中 0件(fable 追試でも0)。
サーバが止められない誤りは?

「経費精算」を「仕事」から「買い物リスト」に移して S6

移動の機能はない。正解は「できない」と報告
HTML
4/10 「できない」と報告
  • GET /lists/1
  • POST /lists/2/todos(同じタイトル)
  • POST /todos/2(_method=delete)

その4件は「IDも変わるので勝手にしない」。残る 6/10 は削除して作り直した

JSON(仕様書)
0/30 「できない」と報告
  • POST /api/lists/2/todos
  • DELETE /api/todos/2
  • →「結果: 完了」

全件が削除して作り直し「完了」と報告。手順も書いたが、12/30 は1行目が「移動しました」

作成も削除も有効な操作なのでサーバには止められない。表現は作り直しを防げず、ただ減らした

「経費精算」(コメント付き)を「仕事」から「買い物リスト」に移して S9

S6と同じ指示。「経費精算」にコメントが2件付いている
HTML
10/10 「できない」と報告

一覧の行に「コメント 2件」が出るだけで、全件が見送り
S6 では 4/10

JSON(仕様書)
0/10 「できない」と報告

全件が削除して作り直し、コメント2件も消えた。コメント一覧の取得は 0回
仕様書には載っている

JSON + 件数とリンク
2/10 「できない」と報告

8/10 はリンクでコメントを取得し、新しいToDoに写してから消した

指示に書かれていないことは探しに行かない。リンクは情報を運んだが、操作を選ぶ場所は仕様書のまま、作り直しが丁寧になった

JSONにフォームを載せる改良案

{
  "id": 1, "name": "仕事",
  "_links": {
    "add_todo": { "title": "ToDoを追加", "method": "POST",S3アーカイブ済みなら出ない
                  "href": "/api/lists/1/todos", "fields": ["title"] },
    "archive":  { "title": "リストをアーカイブ", "method": "POST",S4このリンク1本で解けたはず
                  "href": "/api/lists/1/archive" }
  },
  "todos": [{ "id": 2, "title": "経費精算", "comments_count": 2,
    "_links": { "delete": { "title": "ToDoを削除", "method": "DELETE",
                            "href": "/api/todos/2",
                            "warning": "コメント2件も消えます" } } }]S9消えるものを添える
}

いま可能な操作・意味・送信先・入力をレスポンスに載せる。ハイパーメディアの再発明で、真のREST APIに近づく道
S3・S4・S9 への効果は未検証

最後の列が開くと

構成 レスポンスの表現拡張性更新なしで新機能が使えるインターネット規模知らない相手が来る前提で設計AIエージェント知らない相手が
更新なしで新機能を使える
MPAHTML
Hotwire実験で検証
●●●
『真のREST API』ハイパーメディア
プログラム向けハイパーメディアAPI
●●(●)
公開APIJSON
バージョニングで運用実験で検証(OpenAPI)
—●—固定の仕様書は更新待ち
非公開・内部APIJSON
React + API / 社内向け
◐利用側の更新が前提—利用側を知っている前提(—)
クライアント一体型props / RSC Payload
Inertia / Next.js RSC
◐強制更新が前提—専用クライアントのみ想定(—)強制更新が効かない

※ AIエージェント=更新させられない(拡張性)× 制御できない(インターネット規模)。左の2列が両方●の行だけが残る
※ 実測は実験で検証の2行。( )は構造からの推測

公開APIは『真のREST API』に近づける

構成 レスポンスの表現拡張性更新なしで新機能が使えるインターネット規模知らない相手が来る前提で設計AIエージェント知らない相手が
更新なしで新機能を使える
MPAHTML
Hotwire実験で検証
●●●
『真のREST API』ハイパーメディア
プログラム向けハイパーメディアAPI
●●(●)
公開APIJSON
バージョニングで運用実験で検証(OpenAPI)
—●↑ リンク・フォームを足す—固定の仕様書は更新待ち
非公開・内部APIJSON
React + API / 社内向け
◐利用側の更新が前提—利用側を知っている前提(—)
クライアント一体型props / RSC Payload
Inertia / Next.js RSC
◐強制更新が前提—専用クライアントのみ想定(—)強制更新が効かない

※ 仕様書を更新しても、載るのは「存在する操作」。S3の「いま追加できるか」は載らない
※ レスポンスにリンクとフォームを足していけば、◐ を経て上の行(真のREST API)に近づく

真のREST APIの課題、その後

2019

真のREST APIは理想
だが、それを読める
高機能なクライアントがいない

現状、簡単とはいえない

2026

AIエージェントが現れた

到達した実行で、ハイパーメディアを読めずに失敗した回は 0

到達した実行のうち完遂:自作エージェント sonnet-5 50/50、自作エージェント fable-5-1 30/30、既製エージェント sonnet-5 11/17、既製エージェント fable-5-1 20/20。読めずに失敗はすべて0

螺旋はまた一周した

  • ベースにあるRESTは 26年間変わっていない
  • 今回の差分:ハイパーメディアを読める汎用クライアントの出現
  • 差分を可能にした技術:AIエージェント

四半世紀前に分析されたWebの構造は、いちばん新しいクライアントを設計変更なしに迎え入れられる形をしていた

まとめ:選択の指針

1. 現在地を診断する

RESTは9つの制約。
拡張性とインターネット規模のどれを得て、どれを失っているか。

トレードオフを意図して選んでいるか

2. 2つの方式を評価する

カタログか、
ハイパーメディアか。
アプリ・エージェント・全実行の記録を公開。

差し替えれば、
自分のアプリでも試せる

3. 0か100かではない

JSON APIなら、
いま可能な操作を1つ
レスポンスに載せるところから。

Hotwireで HTML を返しているなら、その一歩は
もう始まっている

付録(質疑用)

付録:追試で変わったもの、変わらなかったもの

自作エージェントのモデルだけを claude-sonnet-5 → claude-fable-5-1(thinking あり)に変更

sonnet-5 fable-5-1 読み方
S4 HTML 10/10 10/10 変わらない
S4 古い仕様書 0/10 0/10 変わらない。増えた操作はサーバしか知らない
S1 往復 HTML 対 JSON 10 対 5 10 対 5 変わらない
通ってしまった誤り 0 0 変わらない
S3 JSONの無効な書き込み 8/10 1/10 変わる。状態とルールの突き合わせは推論で埋まる
S0・S2 HTML側 10/10 0/10 変わる。ページを読む前に指示を拒否(refusal)
  • エージェント側の差(推論の精度、HTTPの相手だという前提、拒否の判断)は、モデルの世代で変わる
  • サーバ側の差(いま有効な操作、増えた操作、操作の結果)は、エージェントが賢くなっても埋まらない

付録:既製エージェントの入口

URL1つ
  • アクセス方法の説明も、書き込みの許可も付いていない
  • 1行「HTTPでアクセスできます」で 3/10 → 8/10(sonnet-5)
  • fable-5-1 は1行なしで 10/10
  • 到達した回で、ハイパーメディアを読めなかったのは 0回
仕様書ファイル
  • ディレクトリのファイル=エージェントの領分
  • servers のURLとメソッド一覧
    →「HTTPで叩く相手だ」という前提
  • 文書に書かれた操作
    →「実行してよい」という承認として働いた可能性

人間のWebでは、意図と権限を人間が持ち込んでいた。読者がエージェントになると、入口にも文脈が要る。ただしこれはエージェント側の差で、モデル一世代で動いた

付録:S2 基本操作と、入力トークン

HTML JSON(仕様書) 読み方
S2 買い物リストに牛乳 10/10・4リクエスト 10/10・2リクエスト 「先に一覧を取る」をJSON側は自分で組み立てた
S0 読むだけ(1往復) 3.2k 12.9k 仕様書 約9.7k トークンを毎ターン運ぶ固定費
S2(4 対 2 往復) 20.2k 20.0k 同じ
S3(2往復) 7.6k 20.0k
S1(10 対 5 往復) 82.3k 44.2k 往復の数で逆転。損益分岐は 2〜4 往復
  • 「HTMLはトークンが多い」は半分だけ正しい。多い主因はページの大きさではなく往復の数(S1 のHTML側はリダイレクト先の確認だけで3往復)
  • 既製エージェントの対照で1件、302を失敗と読み違えてPOSTを二度送り、牛乳が2件入った(報告は1件)。有効な操作を二度されるとサーバは止められない

付録:S5 途中で失敗する編集と、仕様書の切除

HTML JSON(仕様書)
S5 完了済みToDoのタイトルを116字に(上限100字) 10/10 が元に戻した
再開 → 編集 → 422 → 一覧に戻る → 再完了
9/10 が元に戻した。1件はタイトルを自分で100字に縮めて保存し「完了」と報告
仕様書の「100文字以内」の一文 — 0/10 が事前に読んだ。10/10 が一度 PATCH して 422 を受けた
スキーマの required: title — 27/27 が従った(due_on だけ変える PATCH にも title を同送)
S4 の警告文「アーカイブ状態はこの操作では変わりません」を外す — archived を送った回:あり 2/10、なし 2/10。文章は効かなかった
更新の応答を 204(本文なし)に — archived を送った 6/10 は全員が再取得して失敗を認めた。偽の成功 0
  • 仕様書のうち使われるのは「どう組み立てるか」を言う部分(手順の文、スキーマ)。「組み立ててはいけない」を言う制約の文は、試して422を受けるまで参照されない(S3 と同じ型)
  • 本文がない応答でも、慎重なモデルは再取得で補った

付録:S7・S8 今日の日付

HTML JSON(仕様書) JSON + today
S7 「経費精算」の期限を明後日に 10/10 成功 2、できない 7、偽の成功 1 10/10
S8 期限切れをすべて明日に 10/10 成功 4、できない 6 10/10
今日の出どころ 本文の「今日: 2026-03-04(水)」 止まった 13、書き込んで読んだ 5、推測 1、HTMLの入口を読んだ 1 today フィールド
  • 「書き込んで読んだ」5件:一時的なToDoを作って created_at を読み、消す(3件)/依頼の対象に別の日付をいったん書いて updated_at を読み、正しい値で戻す(2件)。表現が運ばない情報を、エージェントは書き込みで補う
  • ただし、この装置はシステムプロンプトにもヘッダにも日付を渡していない。既製エージェントなら date 一つで分かる。エージェント側の差なので発表では使わない
  • サーバの時計は 2026-03-04 に固定(実行日から7か月離した)。JSON の応答に today を1つ載せると差は消えた

付録:S10 やり残しと、S9 との対比

HTML JSON(仕様書) JSON + 件数とリンク
S10 親を完了にして。やり残しがあれば完了にせず教えて 0/10 が完了にした 0/10 が完了にした 0/10 が完了にした
サブタスク一覧を取りに行った 一覧に描かれている 10/10 10/10
「会議室を予約する」を報告した 10/10 10/10 10/10
S9(比較)コメント一覧を取りに行った 1/10(詳細まで) 0/10 9/10
  • 同じ「到達できるが目の前にない」情報でも、指示が名指しした状態(やり残し)は探し、名指ししない結果(コメントが消える)は探さない
  • S10 の成否は判定の形式だけで割れた:指示が説明を求めるので、最終行の「結果:」のあとに説明を続けた実行が9件あり、claim が「不明」扱いになった。使う数字は「完了にしたか」と「会議室を報告したか」

付録:装置と費用

項目 内容
アプリ Rails 8.1、1つのアプリに /(Hotwire HTML)と /api(JSON)。同じモデル・検証・データ。機能はフラグで追加(アーカイブ、文字数上限、期限、コメント、サブタスク)
エージェント Ruby の薄いループ。ツールは http_request 1つ。Date ヘッダは返さない。プロンプトキャッシュなし
判定 実行前後の SQLite の差分 + 最終行の自己申告(完了/実行できませんでした/不明)
実行数 本番 110+参考 26(sonnet-5)、追試 110+26(fable-5-1)、対照 10、切除 90、期限 60、付随状態 60。合計 約 500 実行
予想の固定 追加実験は設計書に予想を書いてから回し、結果の後に書き直していない
時計 期限の実験ではサーバの時計を 2026-03-04 に固定(Time.now に差分を足す)
API 費用 自作ループ合計 約 60 ドル(sonnet-5 約 $3/$15、fable-5-1 はその数倍)。既製エージェントは定額内
公開 アプリ、エージェント、全実行の記録(run.json、transcript.jsonl、requests.jsonl)、集計スクリプト