Go back

Flueで「タスク追加くん」を作った話

Posted on:

個人用のタスク管理エージェントを、Flue + Next.js + Notion で作ってみたログ。以前Flueの読書メモを書いたあと、実際に手を動かしてみた記録でもある。

自然言語で「明日までに資料作成のタスク追加して」と投げるだけで、Gemini経由でNotionのTasksに反映される。画面からもチャットからも同じデータストアを触れるようにした。

技術スタック

pnpmワークスペースで3パッケージ構成にした。

  • apps/agent — Flue(@flue/runtime / @flue/cli, 1.0.0-beta.9)。flue devで動く独立した開発サーバー。
    • LLM: 最初はAnthropic Claude Sonnetで組んだが、無料枠で回したかったので google/gemini-2.5-flash(Gemini API, 無料枠あり)に変更。Flueが内部で使っている「Pi」というプロバイダ抽象化レイヤーのおかげで、モデル指定の文字列(provider/model-id)を書き換えるだけで済んだ。
    • ツール定義は defineTool({ name, description, input, output, run }) 形式(valibotでスキーマ定義)。
  • apps/web — Next.js 16(App Router)+ Tailwind CSS 4。
    • タスク一覧画面: Notion APIを直接叩くCRUD(@notionhq/client)。
    • チャット画面: 当初は @flue/reactuseFlueAgent フックを使っていたが、後述の理由でやめて素の fetch ポーリングに書き換えた。
  • packages/notion-tasks — 両アプリで共有するNotion連携ロジック(データソースID・プロパティ変換・CRUD関数)。agent側のツールとweb側のAPI Routeの両方から使う。
  • データストア: Notionの既存「Tasks」データベースをそのまま使用(Task name / Status / Priority / Due / Tags / Summary のみ対象)。

grill-meの使用感

要件を詰めるのに mattpocock/skillsgrill-me を使った。一問ずつ、推奨案付きで聞いてくるスタイル。概要は azukiazusa.dev の解説記事 がわかりやすい。

grill-meのQ1。用途を問い、推奨案として個人タスク管理エージェントなどを例示

最初の問いが「このAIエージェントは何のためのものか」。推奨の例に「個人タスク管理エージェント」が並んでいて、そのまま今回の題材になった。

grill-meのQ5。Notion DBの対象プロパティを基本項目に絞る推奨

途中の問いでは、既存Tasks DBにある Sprint / Project / GitHub PR といったリレーションをエージェントの対象から外し、Task name / Status / Priority / Due / Tags / Summary だけに絞る判断を迫られた。個人用途なら複雑なRelationはオーバーキル、という推奨に乗った結果が、冒頭の技術スタックに書いた対象プロパティそのものだ。

正直「質問攻め」に感じる場面はあった。ただ、一人でサクッと決め打ちしたいときより、仕様をがっちり固めたいとき・モブワーク(複数人で画面を見ながら決めていくスタイル)に向いているという感触を持った。一問一答なので議論が発散せず、その場にいる全員が同じ理解に揃った状態でコードに入れる。

grill-me の根っこにあるのは「設計ツリー」という考え方だ。『デザインのためのデザイン』(フレデリック・P・ブルックス Jr.)に書かれている部分で、ある判断を下すとその判断に依存する次の問いが枝のように広がっていく。一問ずつ枝を辿るから、質問の対象は広がっていく。一方で、AIに任せる部分と人がきちんと理解して仕様を伝える部分を分けておくと、その戻りや広がりを収束させてくれる感じがした。

既存プロダクトに機能を足す場面では、grill-with-docs のほうが相性が良さそうだ。既存ドキュメント(CONTEXT.md の用語集や ADR)を土台に設計を進め、決まった語彙や取り返しのつかない判断をその場でドキュメントへ残していく。会話だけで終わらず紙の跡が残るので、すでに動いているプロダクトの延長線上で仕様を固めるときに向いている。

Flueを触ってわかったこと

Flueは「TypeScriptで耐久性のあるAIエージェント/ワークフローを書けるオープンフレームワーク」を謳っている。実際に触ってわかったポイントをいくつか。

想定と違った点: Flueは独立したアプリだった

最初は「Next.jsのAPI Routeに組み込む」つもりだった。実際には flue dev という専用の開発サーバーを持つ独立したアプリケーションだった(flue.config.ts / src/agents/ などの専用プロジェクト構成)。

そこでpnpmワークスペースにして、agentとwebを別プロセスとして pnpm dev 一発で両方立ち上げる構成に変更した。

ドキュメントとインストール済みバージョンにズレがあった

公式ドキュメントに載っているツール定義の例(parameters / execute)と、実際にインストールしたbeta版パッケージの型定義(input / output / run)が食い違っていた。

ベータ版のフレームワークだとよくあることなので、node_modules内の型定義ファイルを直接確認して実装を合わせた。

ハマりどころ

  • sqliteの無限リロードループ: 会話履歴を永続化するdb.tsでsqliteファイルをプロジェクト内(data/)に置いたら、開発サーバーの監視対象にDBのWAL/SHMファイルまで入ってしまい、書き込み→変更検知→再起動→書き込み…の無限ループになった。DBファイルをプロジェクト外(.data/)に逃がして解決。
  • SSR時のクライアント初期化エラー: @flue/reactのクライアントをサーバーサイドレンダリング時に相対URLで作るとエラーになる仕様だった。サーバー側は絶対URL、ブラウザ側は相対URL(Next.jsのrewritesでFlueサーバーへプロキシ)を出し分けて解決。
  • @flue/reactのリアルタイム更新が止まる(ベータのバグ): useFlueAgentのSSEストリームが、一度「最新まで追いついた」状態になると更新を受信しなくなる不具合があった。最終的には@flue/react自体をやめて、タスク一覧画面と同じ「素のfetchでポーリング」方式に統一した。
  • Notionの権限: APIキーの認証が通っても、対象データベースをインテグレーション自体に「共有」していないとアクセスできない。地味に忘れがちなNotion側の一手間。

会話履歴の永続化まわり

sqliteの置き場所でハマったので、代替案も一度整理した。結論から言うと、今のユースケース(ローカルで動く個人用ツール)なら現状のsqlite()のままで問題ない。変更する強い理由はない。

補足として、いま使っているsqlite()は外部依存ゼロだった。中身は better-sqlite3 のようなネイティブモジュールではなく、Node.js本体に組み込まれた node:sqliteDatabaseSync)を使っているだけなので、「SQLiteだから重い/ビルドが面倒」という心配は不要だった。

Flueが公式にサポートする永続化はだいたい次のとおり。

方式 実装 向いてるケース
なし(db.tsを削除) インメモリ(再起動で消える) 履歴が消えても気にしない、動作確認だけしたい
sqlite()(現状) node:sqlite、ファイル1つ ローカル・単一プロセスでの個人利用
@flue/postgres postgres(DATABASE_URL 複数プロセス/複数台、Vercel等へのデプロイで共有したい場合
カスタムアダプタ @flue/runtime/adapterを自作 上記以外のDBに繋ぎたい場合

それ以外に flue add database <name> で libsql / mongodb / mysql / postgres / redis / supabase / turso / valkey などが並ぶが、これらは実装済みアダプタではなく、AIコーディングエージェント向けの実装手順書(コード生成の元ネタ)だ。選ぶとエージェントがその手順に従ってコードを書く形になり、多少の作業が発生する。

個人用ローカルアプリで唯一検討する価値があるとすれば Turso(libsqlのホスティング、無料枠あり)くらい。「今はローカルのSQLiteファイルだけど、将来デプロイしても同じ会話履歴を見たい」という場合の移行先として自然で、SQLite互換なのでコードの書き方もほぼ変わらない。クラウドに出す具体的な予定が出たら、その時点で @flue/postgres か Turso に切り替えるのが自然な流れだと思う。

DB周りはいろいろできそうで、会話の永続化が肝になる。別のDBに繋ぐならカスタムアダプタを使う形になるようだ。

実装した機能

  • タスクの一覧表示・追加・ステータス変更・削除(チャットからも画面からも操作可能)
  • チャットからの自然言語操作(「明日までに資料作成のタスク追加して」→ Gemini経由でNotionに反映)
  • 削除は取り消せない操作なので、画面側は確認ダイアログ、エージェント側も「明確な削除指示があった場合のみ実行」という指示をプロンプトに追加

チャット画面は Dispatch というチャンネル名にした。配色やタイポグラフィに強いこだわりはなく、暗めのドットグリッド背景に YOU / AGENT のラベルを乗せただけの薄いUIだが、実際のやりとりはこんな感じになる。

チャット画面。相対日付のタスク追加、「あああ 削除」は拒否、「登壇する 削除」では確認を求める

「明日」「土曜日」といった相対表現は、Geminiが具体日付に落としてからNotionへ書く。削除は曖昧な指示だと受け付けず、対象がはっきりしているときだけ確認を挟む。

削除の確認フロー完了後、続けて「明日 メールを書く」でタスクを作成

確認後に削除が走り、そのまま自然言語で次のタスクも追加できる。チャット履歴の削除はそもそもツール側にないので、聞かれても断る。

チャット履歴削除の要求に対し、機能がないと返す

@AGENTS.md を読み込めばよかった話

create-next-appでNext.jsをインストールしたときに気づいた話。生成されたapps/web/CLAUDE.mdの中身が

@AGENTS.md

の一行だけで、実体は同ディレクトリのAGENTS.md(Next.js 16の破壊的変更に関する注意書き。「これは訓練データにあるNext.jsとは違う」という警告)。

公式ドキュメントにも明記されているとおり、Claude Codeが読み込むのは CLAUDE.md であって AGENTS.md ではない。単体で置いてあっても自動では読まれない。

ただしCLAUDE.mdは「インポート構文」@ファイルパスを使って他ファイルの中身をその場に展開できる。相対パス・絶対パスどちらも使え、インポート先がさらに@...を含んでいれば再帰的に辿る(最大4 hops)。相対パスは作業ディレクトリではなく、インポートを書いたファイル自身からの相対で解決される。

AGENTS.md自体はClaude Code専用ではなく、Codexなど他のコーディングエージェントも読みにいく業界共通規格だ。だから create-next-app は「Codexなど向けにAGENTS.mdを1つ書き、Claude Code向けにはそれを@AGENTS.mdで読み込むだけのCLAUDE.mdを添える」ことで、内容を二重管理せずに済ませている。公式も同じパターンを推奨しており、Claude固有の指示をインポートの下に足すこともできる。

つまり「AGENTS.mdを直接読ませたいなら、CLAUDE.mdから@AGENTS.mdと書いて橋渡しする」というのは正しいテクニックで、地味に賢い運用だった。

まとめ

  • FlueはNext.jsに埋め込むライブラリではなく、flue devを持つ独立アプリ。ワークスペースでagentとwebを分けるのが素直
  • ベータ版はドキュメントと型がズレることがある。詰まったらnode_modulesの型定義を見る
  • @flue/reactのSSEが止まる不具合に当たったら、素のfetchポーリングに逃げてよい
  • 会話履歴の永続化は、個人ローカル用途ならsqlite()node:sqlite)で十分。クラウド共有が必要になったら@flue/postgresかTurso
  • grill-meは質問攻めに感じるが、仕様を固めたいとき・モブで決めるときに効く。根っこは設計ツリー。既存プロダクトならgrill-with-docsのほうが相性が良さそう
  • create-next-appのCLAUDE.md@AGENTS.md一行なのは、Claude Codeが自動で読む対象がCLAUDE.mdだけだから。インポート構文で橋渡しし、二重管理を避けている

参考