AWS Blocks の AGENTS.md で Kiro にチャットアプリを作らせてみた

AWS Blocks の AGENTS.md で Kiro にチャットアプリを作らせてみた

AWS Blocks の Agent Block で AI チャット機能を実装し、ローカルモックから Bedrock 応答へアプリケーションコードの追加変更なしに切り替わる動作を確認しました。プロジェクトルートの AGENTS.md と npm パッケージ内の docs が、Kiro にとって事実上の steering files として機能した点も検証しています。
2026.06.20

はじめに

2026年6月16日、AWS Blocks がプレビュー公開されました。AWS Blocks は、フルスタック TypeScript アプリケーションを「Block」の組み合わせで構築するフレームワークです。

https://aws.amazon.com/about-aws/whats-new/2026/06/aws-blocks-preview/

従来のフルスタック AWS 開発と比較すると、以下のような違いがあります。

観点 従来 AWS Blocks
インフラ定義 CDK / CloudFormation を直接記述 Block を組み合わせ、CDK は内部で自動生成
ローカル開発 LocalStack や SAM Local 等で模倣 npm run dev でローカルサーバーが起動、ローカルモックで検証可能
デプロイ CDK deploy / SAM deploy npm run deploy(内部で CDK)
AI モデル呼び出し Bedrock SDK を直接叩く Agent Block が抽象化。ローカルはモック、本番は Bedrock

今回は、この AWS Blocks の Agent Block を使って AI チャット機能を実装しました。ポイントは2つです。

1つ目は、プロジェクトルートの AGENTS.md と npm パッケージ内の docs/ ディレクトリが、Kiro にとって事実上の steering files として機能し、基本的な Block API の使い方に沿ったコードを生成できたこと(細部には手修正が必要でした)。

2つ目は、手修正後にローカルで動作確認したチャットアプリのアプリケーションコードを、追加変更なしにデプロイし、Bedrock の Claude Haiku 4.5 が応答するようになったことです。

Docker でプロジェクトを生成してローカル起動する

今回は Docker コンテナ内でプロジェクトを生成・実行しました。Docker は必須ではなく、ホストに Node.js 22 があればそのまま実行できます。以降の手順は Docker 前提で記載しています。

docker run -it --rm -v $(pwd)/project:/work -w /work --network host node:22 bash

コンテナ内でプロジェクトを生成します。--yes を指定しているため、テンプレートはデフォルト構成が使用されます。

mkdir my-chat-app && cd my-chat-app
npx @aws-blocks/create-blocks-app@latest . --yes

依存パッケージは生成時に自動インストールされるため、続けて npm run dev を実行できます。

生成されたプロジェクトの構成は以下のとおりです。

my-chat-app/
├── aws-blocks/
│   ├── index.ts          ← バックエンド定義(API, Auth, Data, Agent)
│   ├── index.cdk.ts      ← CDK エントリポイント
│   ├── index.handler.ts  ← Lambda ハンドラー
│   └── scripts/          ← server, deploy, destroy 等のスクリプト
├── src/
│   └── index.ts          ← フロントエンド
├── test/
│   └── e2e.test.ts
├── AGENTS.md             ← AI エージェント向け指示書
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts

npm run dev でローカルサーバーを起動します。

npm run dev
Loading backend...
Deploying local resources...
  🔌 Attaching dev server (from @aws-blocks/bb-realtime/ws-server)
  🔌 Attaching dev server (from @aws-blocks/bb-file-bucket/file-server)
  🔌 Attaching dev server (from @aws-blocks/bb-realtime/ws-server)
📝 Generating client code...
AWS Blocks local server running on http://localhost:3000

この時点で AWS credentials は不要です。今回使った Block ではローカル用のモックプロバイダーが自動適用され、ブラウザから http://localhost:3000 にアクセスすると Todo アプリのサンプルが動作します。

検証時のバージョンは以下のとおりです。

パッケージ バージョン
@aws-blocks/blocks 0.1.4
@aws-blocks/bb-agent 0.1.2
@aws-blocks/create-blocks-app 0.1.3
Node.js 22.22.3
AWS CDK 2.260.0

AGENTS.md と docs — Kiro にとっての事実上の steering files

AGENTS.md と npm パッケージ内 docs の構成

プロジェクト生成時、ルートに AGENTS.md が配置されます。これは AI エージェント向けの指示書で、node_modules/@aws-blocks/blocks/docs/ 配下のドキュメントを参照するよう記述されています。

node_modules/@aws-blocks/blocks/ 配下には docs/ ディレクトリが存在します。

プロジェクトルート/
├── AGENTS.md              ← AI エージェント向けの指示書(create-blocks-app が配置)

node_modules/@aws-blocks/blocks/
├── README.md              ← アーキテクチャ・ワークフロー・ベストプラクティス
├── docs/
│   ├── index.md           ← Block カタログ
│   ├── bb-agent.md        ← Agent Block のドキュメント(約34KB)
│   └── ...
├── src/
└── package.json           ← "files" に "docs" が明示的に含まれる

package.json"files" フィールドに "docs" が含まれており、npm publish 時に意図的に同梱されています。ただし、公式にはこれらを「steering files」とは呼んでいません。AGENTS.md からは「Full guide」「Block catalog」「Per-block docs」として参照されています。

プロジェクト生成時にルートに置かれる AGENTS.md の内容は以下のとおりです。

AGENTS.md 全文
# Agent Guide

## Quick Reference

- **Backend:** `aws-blocks/index.ts` — APIs, auth, data models
- **Frontend:** `src/` — imports backend APIs via `import { api } from 'aws-blocks'`
- **Tests:** `test/e2e.test.ts` — run with `npm run test:e2e`
- **Full guide:** `node_modules/@aws-blocks/blocks/README.md` — architecture, workflow, best practices, common mistakes
- **Block catalog + decision tree:** `node_modules/@aws-blocks/blocks/docs/index.md`
- **Per-block docs:** `node_modules/@aws-blocks/blocks/docs/<package-name>.md`

## Workflow

1. Make changes to backend (`aws-blocks/index.ts`) or frontend (`src/`)
2. Test with `npm run test:e2e` — starts a dev server automatically if one isn't running
3. For faster iteration: run `npm run dev &` in the background, then run `npm run test:e2e` repeatedly (reuses the running server)
4. Do NOT use curl/fetch against the API unless troubleshooting connectivity

## Rules

- **Use Building Blocks** for all persistence and cloud abstractions — never local files, in-memory arrays, or local databases.
- **Read block docs** at `node_modules/@aws-blocks/blocks/docs/<package-name>.md` before using a block.
- **The JSON-RPC transport is invisible** — do not construct RPC payloads manually. Import and call the typed API directly.

## Deploying (requires AWS credentials)

- `npm run sandbox` — deploy backend to AWS, serve frontend locally
- `npm run deploy` — full production deploy to AWS
- `npm run sandbox:destroy` — tear down sandbox resources

ポイントは「Read block docs at node_modules/@aws-blocks/blocks/docs/<package-name>.md before using a block.」の一文です。AI エージェントに「Block を使う前にドキュメントを読め」と指示しています。

Kiro にチャット機能を実装させる

別の Kiro セッションをプロジェクトディレクトリで起動し、以下のプロンプトを投入しました。

このプロジェクトは AWS Blocks で作成されたアプリケーションです。

以下の機能を実装してください:

1. Agent Block を使った AI チャット機能を追加する
2. モデルは Claude Haiku 4.5 を使用する(Bedrock で利用可能な正確な model ID を確認して指定してください)
3. フロントエンドにチャット UI を追加する(メッセージ入力欄、送信ボタン、会話履歴表示)
4. ユーザー認証(AuthBasic)を維持し、認証済みユーザーのみがチャット API を呼び出せるようにする

要件:
- aws-blocks/index.ts にバックエンド API(sendMessage)を追加
- src/index.ts にチャット UI を追加
- ローカル実行(npm run dev)でダミー応答が返ること
- デプロイ後は Amazon Bedrock 経由で Haiku 4.5 が応答する構成になること

実装時の注意:
- node_modules/@aws-blocks/blocks/ 配下にドキュメントや steering files があれば参照して、正しい Block API の使い方に従ってください
- public API から import してください(internal path や undocumented API は避ける)
- リージョンは ap-northeast-1 を前提としてください

Kiro が関連ドキュメントを参照した流れ

Kiro はプロジェクトルートの AGENTS.md を検出し、さらにプロンプト内の「ドキュメントがあれば参照して」という指示も踏まえて、以下のファイルを読み込みました(セッションの操作ログで確認)。

  • node_modules/@aws-blocks/blocks/docs/bb-agent.md(Agent Block の API 仕様、約34KB)
  • node_modules/@aws-blocks/blocks/docs/index.md(Block カタログ)
  • node_modules/@aws-blocks/blocks/src/index.ts(re-export 一覧)
  • node_modules/@aws-blocks/bb-agent/dist/index.hooks.d.ts(型定義)
  • node_modules/@aws-blocks/bb-agent/package.json(client export 確認)

AGENTS.md に「node_modules/@aws-blocks/blocks/docs/<package-name>.md を読め」と明記されていたことに加え、プロンプトでも「配下にドキュメントがあれば参照して」と指示していたため、Kiro はこれらを踏まえてドキュメントを参照しました。少なくとも今回のセッションでは、ファイル内容を手動で貼り付けてコンテキストとして渡す必要はありませんでした。

生成されたコード(最終版)

手修正8件を適用した後の最終的なコードです。

aws-blocks/index.ts 全文
/**
 * Backend — aws-blocks/index.ts
 */
import { ApiNamespace, Scope, AuthBasic, DistributedTable, Realtime, Agent } from '@aws-blocks/blocks';
import { z } from 'zod';

const scope = new Scope('my-app');

// ─── Auth ────────────────────────────────────────────────────────────────────
const auth = new AuthBasic(scope, 'auth', {
  passwordPolicy: { minLength: 8 },
  crossDomain: process.env.BLOCKS_SANDBOX === 'true',
});
export const authApi = auth.createApi();

// ─── Data ────────────────────────────────────────────────────────────────────
const todoSchema = z.object({
  userId: z.string(),
  todoId: z.string(),
  title: z.string(),
  completed: z.boolean(),
  priority: z.number(),
  version: z.number(),
  createdAt: z.number(),
});

const todos = new DistributedTable(scope, 'todos', {
  schema: todoSchema,
  key: { partitionKey: 'userId', sortKey: 'todoId' },
  indexes: {
    byPriority: { partitionKey: 'userId', sortKey: 'priority' },
    byTitle: { partitionKey: 'userId', sortKey: 'title' },
  },
});

// ─── Realtime ────────────────────────────────────────────────────────────────
const rt = new Realtime(scope, 'live', {
  namespaces: {
    todos: Realtime.namespace(z.object({
      action: z.enum(['created', 'updated', 'deleted']),
      todoId: z.string(),
    })),
  },
});

// ─── Agent ────────────────────────────────────────────────────────────────────
const agent = new Agent(scope, 'chat-agent', {
  inferenceOnly: true,
  model: { deployed: { provider: 'bedrock', modelId: 'global.anthropic.claude-haiku-4-5-20251001-v1:0' } },
  systemPrompt: 'You are a helpful assistant. Answer concisely in the same language the user writes in.',
});

// ─── API ─────────────────────────────────────────────────────────────────────
export const api = new ApiNamespace(scope, 'api', (context) => ({

  async subscribeTodos() {
    const user = await auth.requireAuth(context);
    return rt.getChannel('todos', user.username);
  },

  async createTodo(title: string, priority: number = 2) {
    const user = await auth.requireAuth(context);
    const todoId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
    const todo = {
      userId: user.username,
      todoId,
      title,
      completed: false,
      priority,
      version: 1,
      createdAt: Date.now(),
    };
    await todos.put(todo);
    await rt.publish('todos', user.username, { action: 'created' as const, todoId });
    return todo;
  },

  async listTodos(sortBy?: 'priority' | 'title') {
    const user = await auth.requireAuth(context);
    if (sortBy) {
      const index = sortBy === 'priority' ? 'byPriority' : 'byTitle';
      return await Array.fromAsync(
        todos.query({ index, where: { userId: { equals: user.username } } })
      );
    }
    return await Array.fromAsync(
      todos.query({ where: { userId: { equals: user.username } } })
    );
  },

  async toggleTodo(todoId: string) {
    const user = await auth.requireAuth(context);
    const todo = await todos.get({ userId: user.username, todoId });
    if (!todo) throw new Error('Todo not found');
    await todos.put(
      { ...todo, completed: !todo.completed, version: todo.version + 1 },
      { ifFieldEquals: { version: todo.version } },
    );
    await rt.publish('todos', user.username, { action: 'updated' as const, todoId });
    return { success: true };
  },

  async updatePriority(todoId: string, priority: number) {
    const user = await auth.requireAuth(context);
    const todo = await todos.get({ userId: user.username, todoId });
    if (!todo) throw new Error('Todo not found');
    await todos.put(
      { ...todo, priority, version: todo.version + 1 },
      { ifFieldEquals: { version: todo.version } },
    );
    await rt.publish('todos', user.username, { action: 'updated' as const, todoId });
    return { success: true };
  },

  async deleteTodo(todoId: string) {
    const user = await auth.requireAuth(context);
    await todos.delete({ userId: user.username, todoId });
    await rt.publish('todos', user.username, { action: 'deleted' as const, todoId });
    return { success: true };
  },

  // ─── Chat ──────────────────────────────────────────────────────────────────
  async sendMessage(message: string) {
    await auth.requireAuth(context);
    const result = await agent.stream(message);
    const done = await result.complete();
    return { reply: done.text };
  },
}));
src/index.ts 全文
/**
 * Frontend — src/index.ts
 *
 * AuthBasic + AI Chat.
 */
import { api, authApi } from 'aws-blocks';
import { AccountMenuBar, AuthenticatedContent } from '@aws-blocks/blocks/ui';
import { html, render } from 'lit-html';

// Auth menu bar
document.getElementById('menu-bar')!.appendChild(AccountMenuBar(authApi));

// Chat UI (shown when authenticated)
document.getElementById('app')!.appendChild(
  AuthenticatedContent(authApi, (user) => {
    const container = document.createElement('div');
    type Message = { role: 'user' | 'ai'; text: string };
    let messages: Message[] = [];
    let loading = false;
    let inputValue = '';

    function redraw() {
      render(html`
        <h2>💬 AI Chat</h2>
        <p style="color:#666;font-size:0.85em">Logged in as: ${user.username}</p>
        <div style="border:1px solid #ddd;border-radius:8px;padding:16px;max-height:400px;overflow-y:auto;margin-bottom:12px;background:#fafafa">
          ${messages.length === 0
            ? html`<p style="color:#999;text-align:center">Send a message to start chatting</p>`
            : messages.map(m => html`
              <div style="margin:8px 0;padding:8px 12px;border-radius:8px;max-width:80%;${m.role === 'user' ? 'margin-left:auto;background:#e3f2fd;text-align:right' : 'background:#f5f5f5'}">
                <strong>${m.role === 'user' ? 'You' : 'AI'}:</strong> ${m.text}
              </div>
            `)}
          ${loading ? html`<div style="color:#999;padding:8px">Thinking...</div>` : ''}
        </div>
        <div style="display:flex;gap:8px">
          <input
            type="text"
            placeholder="Type a message..."
            style="flex:1;padding:8px;border:1px solid #ddd;border-radius:4px"
            .value=${inputValue}
            @input=${(e: Event) => { inputValue = (e.target as HTMLInputElement).value; }}
            @keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter' && !loading) send(); }}
          />
          <button @click=${send} ?disabled=${loading}>Send</button>
        </div>
      `, container);
    }

    async function send() {
      const msg = inputValue.trim();
      if (!msg) return;
      inputValue = '';
      messages = [...messages, { role: 'user', text: msg }];
      loading = true;
      redraw();
      try {
        const { reply } = await api.sendMessage(msg);
        messages = [...messages, { role: 'ai', text: reply ?? '' }];
      } catch (e: any) {
        messages = [...messages, { role: 'ai', text: `Error: ${e.message}` }];
      }
      loading = false;
      redraw();
    }

    redraw();
    return container;
  })
);

なお、index.html はテンプレート生成時のものをそのまま使用しています(id="menu-bar"id="app" の要素が含まれています)。

この実装では agent.stream() に現在のメッセージのみを渡しているため、各リクエストは独立した1ターンの推論となり、マルチターン会話にはなっていません(inferenceOnly: true により会話履歴の永続化も行いません)。フロントエンド上の会話履歴は表示用に保持しているだけです。

手修正が必要だった箇所

Kiro の生成コードは大枠として正しかったものの、8件の手修正が必要でした。以下、代表的な3件を詳述し、残り5件は表にまとめます。

inferenceOnly: true の追加(API 誤用)

Kiro は当初 inferenceOnly を指定せず Agent を作成しました。この検証では、inferenceOnly 未指定の状態で stream() + complete() を実行すると、会話履歴の永続化を伴う動作になるようで、FileBucket(S3)へのアクセスが発生しローカルではエラーになりました。推論のみでよい今回の用途では inferenceOnly: true を指定しました。

// Before(Kiro 生成)
const agent = new Agent(scope, 'chat-agent', {
  model: { deployed: { provider: 'bedrock', modelId: '...' } },
});

// After(手修正)
const agent = new Agent(scope, 'chat-agent', {
  inferenceOnly: true,  // ← 追加
  model: { deployed: { provider: 'bedrock', modelId: '...' } },
});

model ID のリージョン対応(リージョン設定)

Kiro はドキュメント上のプリセット BedrockModels.FAST を使いましたが、これは検証時点では us.anthropic.claude-haiku-4-5-20251001-v1:0(US リージョン群向けの inference profile)を指していました。ap-northeast-1 からは global. prefix の global inference profile を指定する必要がありました。

// Before
model: { deployed: BedrockModels.FAST },

// After
model: { deployed: { provider: 'bedrock', modelId: 'global.anthropic.claude-haiku-4-5-20251001-v1:0' } },

ファイル形式の修正

Kiro は src/app.tsx を生成しましたが、このプロジェクトは lit-html ベースで JSX を使いません。src/app.tsx を削除し、src/index.ts に統合しました。

その他の修正一覧:

分類 件数 内容
型エラー 3 ChatMessage vs 独自 Message 型の不一致、subscribe ハンドラーの型キャスト、reply が undefined になり得る型へのフロントエンド側での対応
import 誤り 1 bundler moduleResolution で拡張子 .tsx 指定不可
構成問題 1 SSH トンネル経由では WebSocket を確立できず、チャット機能は HTTP RPC の sendMessage で検証

AGENTS.md とプロンプト内の指示により関連ドキュメントが参照されたことで、Agent Block の基本的な API(new Agent() の構成、agent.stream() の呼び出しパターン)はおおむね正しく生成されました。一方で、リージョン固有の Bedrock 設定や inferenceOnly 未指定時の永続化を伴う挙動などは、今回の生成結果だけでは吸収しきれず手修正が必要でした。

ローカルでモック応答を確認する

手修正を適用した後、npm run dev でローカルサーバーを起動します。ブラウザからメッセージを送信すると、Agent Block の CannedProvider が自動適用され、以下のようなモック応答が返ります。

You: test
AI: This is a canned mock response. No real model was called. [canned]

なお、今回のローカル dev 環境ではユーザーデータはインメモリで保持されるため、dev server を再起動するとアカウントが消えます。再度 Sign Up が必要です。

ローカル確認済みのコードを AWS にデプロイする

ローカルで動作確認できたコードを、アプリケーションコードの変更なしにデプロイします。

前提条件

デプロイには以下が必要です。

  • AWS アカウントで CDK bootstrap が完了していること(npx cdk bootstrap aws://ACCOUNT_ID/ap-northeast-1
  • デプロイ用の IAM principal に CloudFormation、IAM、Lambda、DynamoDB、S3、CloudFront、Bedrock 関連の権限があること
  • Bedrock の model access で Claude Haiku 4.5 が有効化されていること
  • global inference profile(global. prefix)が利用可能であること

デプロイ実行

Docker コンテナに AWS credentials を渡してデプロイします。

docker run -it --rm \
  -v $(pwd)/project/my-chat-app:/work -w /work \
  -e AWS_ACCESS_KEY_ID=XXXX \
  -e AWS_SECRET_ACCESS_KEY=XXXX \
  -e AWS_SESSION_TOKEN=XXXX \
  -e AWS_DEFAULT_REGION=ap-northeast-1 \
  node:22 bash -c "npm run deploy"

今回の検証では、初回デプロイは約10分で完了しました。DynamoDB の GSI 作成と CloudFront ディストリビューションの配布に時間がかかります。作成された CloudFormation リソースは約105個でした。更新デプロイは約4分です。

Bedrock からの実応答を確認

デプロイ後、CloudFront 経由の URL にアクセスします。AuthBasic による認証画面でログインした後、チャット画面から同じようにメッセージを送信すると、今度は Claude Haiku 4.5 からの実応答が返ります。

ローカルでは This is a canned mock response. No real model was called. [canned] だった応答が、デプロイ後は Bedrock 経由の実際の回答に切り替わりました。ローカルで動作確認した最終版の aws-blocks/index.tssrc/index.ts を、デプロイのために追加変更する必要はありませんでした。

今回は東京リージョン(ap-northeast-1)へのデプロイのため、model ID には global inference profile の global.anthropic.claude-haiku-4-5-20251001-v1:0 を指定しています。

クリーンアップ

検証後はスタックを削除します。npm run deploy で作成したリソースは npm run destroy で削除します(sandbox の場合は npm run sandbox:destroy)。

npm run destroy

まとめ

AWS Blocks プロジェクトに生成される AGENTS.md と、npm パッケージに同梱された docs/ は、公式な steering files ではないものの、今回の検証では Kiro 向けの参照資料として有効に機能しました。Kiro は関連ドキュメントや型定義を参照し、Agent Block の public API に沿ったコードをおおむね生成できました。

ただし、生成コードをそのまま使えたわけではなく、inferenceOnly: true の追加、ap-northeast-1 向けの global inference profile の指定、lit-html 構成への調整など、最終的に8件の手修正が必要でした。基本的な API の使い方は出力できた一方で、リージョン固有の Bedrock 設定や永続化モードの挙動は手修正が必要なポイントでした。

手修正後の同じアプリケーションコードで、ローカルでは CannedProvider によるモック応答、デプロイ後は Bedrock 経由の Claude Haiku 4.5 の実応答に切り替わることを確認できました。ローカルで UI とロジックを固め、そのまま実モデル接続へ進められる点は、AI 機能の検証を進めやすくする体験でした。

参考リンク

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事