スキルファーストアーキテクチャ

ThemisはすべてのAIエージェント機能を3つのレイヤーに構造化しています:スキル(ドメイン知識)、出力ツール(構造化コントラクト)、ワークフロー(ライフサイクルオーケストレーション)。スキルが基盤であり、エージェントを効果的にする判断力、推論力、専門知識を持っています。

3つのレイヤー

+--------------------------------------------------+
|  LAYER 1: SKILL                                  |
|  Owns: persona, process, judgment, domain knowledge |
|  Sources: file-based (.claude/skills/, lib/skills/) |
|           DB-backed (Skill model, 3 scopes)       |
|           prompts (app/prompts/)                  |
+--------------------------------------------------+
                       |
                 agent calls tool
                       |
                       v
+--------------------------------------------------+
|  LAYER 2: OUTPUT TOOL                            |
|  Owns: structured data contract, side effects    |
|  Lives in: app/services/*_tool_builder.rb        |
|  Built with: ClaudeAgentSDK.create_tool()        |
+--------------------------------------------------+
                       |
                 returns result
                       |
                       v
+--------------------------------------------------+
|  LAYER 3: WORKFLOW                               |
|  Owns: lifecycle orchestration only              |
|  Lives in: app/services/workflows/               |
|  Target: under 50 lines                          |
+--------------------------------------------------+

レイヤー1:スキル

スキルはすべての判断を担います。エージェントに_何を_すべきか、_どのように_推論すべきか、_いつ_ツールを使うべきかを伝えます。Themisは3つのスキルソースをサポートしており、それらが連携してエージェントに包括的なドメイン知識を提供します。

ファイルベーススキル(コードベース)

SKILL.mdマニフェストを持つmarkdownファイルとしてリポジトリにチェックインされたスキルです。2つのディレクトリが異なる目的を果たします:

  • .claude/skills/ — Themis内部スキル。アーキテクチャガイド、コーディング規約、レビュー手法、インテグレーションヘルパー。Themisリポジトリに留まり、Claude Agent SDKによって自動検出されます。
  • lib/skills/ — ポータブルスキル。コード生成時にターゲットプロジェクトのワークツリーにコピーされ、エージェントがクロスプロジェクトの標準を持ち運べるようにします(例:Railsの規約やセキュリティチェックリストを含むcode-quality/)。

各スキルはSKILL.md(YAMLフロントマター + markdown)とオプションの補足ファイルを含むディレクトリです:

.claude/skills/understanding-themis/
  SKILL.md          # 名前、説明、コンテンツを含むマニフェスト
  HOTWIRE.md        # 補足リファレンス(オプション)

DBバックスキル(Skillモデル)

Active Storageファイル添付を持つデータベースに保存されたユーザー作成スキルです。Web UIまたはチャット中のエージェントツールを通じて管理されます。3つのスコープが可視性を制御します:

スコープ所有者閲覧可能ユースケース
システム管理者全ユーザー、全スペース組織全体の標準
スペーススペースすべてのスペースメンバーチーム固有の知識
個人ユーザー所有者のみ個人の好みとワークフロー

DBスキルは各エージェント実行前にSkillExtractorによってディスクに抽出され、アトミックなディレクトリ置換でスコープごとにキャッシュされます。エージェントは同じ.claude/skills/ディレクトリ規約を通じてそれらを発見します。

SkillExtractor.prepare_for_agent(space:, user:)
  → Skill.available_for(user, space)をクエリ
  → キャッシュディレクトリに抽出(system / space / personal)
  → SDKオプション用のadd_dirs配列を返す

エージェントはSkillToolBuilderツール(create_skillupdate_skilllist_my_skills)を通じて、会話中に独自の個人スキルを作成・更新することもできます。

エージェントプロンプト

ワークフロー固有の指示を提供する静的および動的プロンプトファイルです:

  • 静的プロンプト.md) — スキルがランタイムコンテキストを必要としない場合。例:レビュープロセス、品質基準、判定基準を含むpr_review.md
  • 動的プロンプト.md.erb) — ランタイムデータを注入するERBテンプレート。例:base_agent.md.erbは space ごとのコンテキスト(エージェント ID、利用可能なチャネルなど)をレンダリングします。

プロンプトはapp/prompts/にあります。PromptLoader.load("name")(静的)またはPromptLoader.render("name", locals)(動的)で読み込みます。

プロンプトはプロセスと判断を定義しますが、出力フォーマットは記述しません — その責任は出力ツールスキーマにあります。

スキルがエージェントに届く方法

ChatJob / ChannelMentionJob
  │
  ├─ System prompt ← PromptLoader (app/prompts/)
  │
  └─ add_dirs ← SkillExtractor
       ├─ File-based skills (.claude/skills/)
       └─ DB skills (extracted to cache)
              ├─ system/
              ├─ {space_id}/space/
              └─ {space_id}/personal/{user_id}/

Claude Agent SDKはadd_dirsSKILL.mdファイルをスキャンし、エージェントに自動的に利用可能にします。スキルはfeature_skills設定でスペースごとにトグル可能です。

レイヤー2:出力ツール

出力ツールはエージェントとシステム間の構造化コントラクトを定義します。エージェントにパース可能なテキストを生成させる(脆弱な)代わりに、型付き引数で呼び出すツールを提供します。

ツールを呼び出し元に接続するのは、ツールを定義した後の次のステップです。各エージェントの呼び出し元(フルエージェント、Web/API チャット、メッセージング)は、ツールカタログ経由でツールグループのセットにオプトインします — 新しい呼び出し元にツールを追加するのは4ファイルにまたがる編集ではなく、宣言的な1回の変更です。

ツールビルダーはapp/services/*_tool_builder.rbにあり、ClaudeAgentSDK.create_tool()を使用します:

class PRReviewToolBuilder
  def self.build_submit_review_tool(review:, space:)
    ClaudeAgentSDK.create_tool(
      "submit_review",
      "Submit your completed code review.",
      {
        type: "object",
        properties: {
          verdict: { type: "string", enum: %w[APPROVE REQUEST_CHANGES COMMENT] },
          summary: { type: "string", description: "Markdown review summary" },
          comments: {
            type: "array",
            items: {
              type: "object",
              properties: {
                path: { type: "string" },
                line: { type: "integer" },
                body: { type: "string" }
              },
              required: %w[path line body]
            }
          }
        },
        required: %w[verdict summary]
      }
    ) do |args|
      # 副作用:GitHubに送信、レビューレコードを更新
    end
  end
end

スキーマフォーマット仕様です。エージェントはツールリストでそれを確認し、何を生成すべきか正確に把握します。出力フォーマットの指示にプロンプトのバジェットを浪費する必要はありません。

現在のツールビルダー

目的別にグループ化。すべてのツールは ツールカタログ 経由でエージェント呼び出し元に接続されます。

ワークフロー出力コントラクト

ビルダー主要ツール目的
PRReviewToolBuilderget_pr_info, get_pr_diff, get_pr_comments, get_ci_status, submit_reviewPRレビューワークフローの出力
CodeGenerationResultToolBuildersubmit_code_generation_resultコード生成からのPRメタデータ
AutomationToolBuilderskip_message自動化のスキップ判断

トリガー(ファクトリー接続)

ビルダー主要ツール目的
PRReviewTriggerToolBuildertrigger_pr_reviewチャット/メンションからPRレビューをエンキュー
CodeGenerationToolBuildertrigger_code_generationチャット/メンションからコード生成をエンキュー

データアクセス

ビルダー主要ツール目的
GithubToolBuilderget_pr_info, get_pr_diff, get_pr_comments, get_ci_status, list_pull_requests, post_pr_commentチャット/メンションコンテキストでのGitHub直接APIアクセス
ChatHistoryToolBuildersearch_conversations, recall_conversationオンデマンド会話履歴
RepoSearchToolBuilderresolve_repo_pathローカル git ワークツリーの参照
ThemisQueryToolBuilderquery_themis_dataThemis DBクエリ(editable_by? ゲート)
GoogleDriveProxyToolBuilderプロキシされたGoogle Drive読み取りツールユーザーごとのOAuthスコープ Drive アクセス

副作用

ビルダー主要ツール目的
SentryToolBuilderupdate_sentry_issueSentry ステータス + アサインメント
MemoryToolBuildersave_memory, delete_memoryユーザーごとのメモリストア

チャット UX

ビルダー主要ツール目的
AskUserQuestionHook (PreToolUse)AskUserQuestion (ネイティブ組み込み)構造化された確認質問 — MCP ツールとして構築せず、フックで介入
FileToolBuildercreate_fileエージェント生成のファイルダウンロード
ShowWidgetToolBuildershow_widgetサンドボックス化されたHTMLウィジェット(D3、Mermaid、SVG)
ShowChartToolBuildershow_chart構造化された Chart.js レンダリング
ImageGenerationToolBuildergenerate_imageGemini 画像生成

リソース管理

ビルダー主要ツール目的
SkillToolBuilder13 ツール — CRUD、ファイル操作、チェックアウト/チェックインエージェント駆動のスキル管理
AutomationChatToolBuildercreate_automation, update_automation, list_my_automations, delete_automationエージェント駆動の自動化管理

レイヤー3:ワークフロー

ワークフローは薄いグルーです。レコードの作成、エージェントの起動、エラー処理、ステータスの更新を行います。判断はゼロパースもゼロであるべきです。

すべてのワークフローはWorkflows::BaseWorkflowを継承し、#executeを実装します。基底クラスは#run_agent(prompt:, system_prompt:, model:, max_turns:)を提供します。

module Workflows
  class FeatureWorkflow < BaseWorkflow
    def execute(input:)
      record = FeatureRecord.create!(input: input, status: "running")

      begin
        result = run_agent(
          prompt: build_prompt(input),
          system_prompt: PromptLoader.load("feature_name")
        )
        record.complete!(result)
      rescue => e
        record.fail!(e.message)
        raise
      end
    end
  end
end

ワークフローで正規表現のパース、JSON抽出、ビジネスロジックを行っている場合、何かが間違ったレイヤーにあります。

判断フレームワーク

質問回答レイヤー
判断、推論、またはドメイン知識を伴うか?スキルに移動スキル
構造化データ交換を定義するか、副作用を生むか?出力ツールにする出力ツール
レコードのライフサイクル、エラーリカバリ、オーケストレーションを管理するか?ワークフローに残すワークフロー
LLMの自由テキストを構造化データにパースしているか?間違っている出力ツールにリファクタリング
出力フォーマットについてのプロンプト指示を書いているか?ツールスキーマが処理すべき出力ツールにリファクタリング
ワークフローが50行を超えているか?何かが間違ったレイヤーにある監査して再配分

新しい機能の追加

ステップ1:スキルを書く

目的に応じてスキルの配置場所を決定します:

  • エージェントプロンプトapp/prompts/) — ワークフロー固有の指示。静的には.md、動的コンテキストには.md.erbを使用。
  • コードベーススキル.claude/skills/) — ワークフロー間で共有される再利用可能なドメイン知識。
  • DBスキル — UIを通じて管理されるユーザー設定可能な知識。

焦点:ペルソナ、プロセス、判断基準、ドメイン知識。出力フォーマットは記述しないでください

ステップ2:出力コントラクトをSDKツールとして定義する

app/services/feature_tool_builder.rbを作成します。ツールスキーマがエージェントが生成するものを定義します。ハンドラーが副作用を実行します。

class FeatureToolBuilder
  def self.build_submit_tool(record:, space:)
    ClaudeAgentSDK.create_tool(
      "submit_result",
      "Submit your analysis results.",
      { type: "object", properties: { ... }, required: %w[...] }
    ) do |args|
      # 副作用を実行、確認を返す
    end
  end
end

ステップ3:ワークフローを薄いグルーとして書く

app/services/workflows/feature_workflow.rbを作成します。レコードの作成、プロンプトの構築、run_agentの呼び出し、エラー処理、ステータスの更新のみを行うべきです。目標は50行未満です。

ステップ4:ジョブに接続する

app/jobs/feature_job.rbを作成します。ジョブがオプション(モデル、MCPサーバー、ツール、スキルディレクトリ)を構築し、ワークフローをインスタンス化してexecuteを呼び出します。mcp_servers / allowed_tools リストを手で組み立てる代わりに、ツールカタログを使ってツールグループにオプトインしてください。

class FeatureJob < ApplicationJob
  def perform(record_id)
    record = FeatureRecord.find(record_id)
    options = build_options(record)
    workflow = Workflows::FeatureWorkflow.new(options: options)
    workflow.execute(record: record)
  end
end

アンチパターン

アンチパターンなぜ間違っているか修正方法
エージェントの自由テキストからJSONをパース脆弱:markdownフェンス、余分なテキスト、フォーマットの変動で壊れる型付き引数を持つ出力ツールを定義
出力フォーマットについてのプロンプト指示エージェントが無視する可能性のあるルールにトークンバジェットを浪費。コントラクトの重複ツールスキーマフォーマット
ワークフロー内のビジネスロジックオーケストレーションをドメインロジックに結合。ワークフローが肥大化判断はスキルに、データコントラクトはツールに移動
ERBなしの巨大なプロンプトランタイムコンテキスト(ユーザー設定、プロジェクト設定)を注入できない動的セクションにはlocalsを持つ.md.erbを使用
ワークフローごとに複数の出力ツールどのツールを呼ぶかエージェントが混乱するワークフローごとに1つの主要出力ツール
複雑なビジネスロジックを持つツールハンドラーテストが困難、インフラストラクチャに密結合ハンドラーをシンプルに:検証、副作用、確認

ワークフロー成熟度

成熟度は、各ワークフローがスキル、出力ツール、オーケストレーションをどれだけクリーンに分離しているかを追跡します。ツール接続は成熟度レベルに関わらず、すべてのワークフローで ツールカタログ 経由で統一されています。

ワークフロー成熟度出力コントラクト
PRReviewsubmit_review SDKツールがGitHub送信を処理
MentionエージェントがMCPツールを直接使用(GitHub、Linearコメント)
Automationskip_messageツールでスキップ判断。AutomationMessageDeliveryServiceで配信
CodeGenerationsubmit_code_generation_resultツールは存在。PRメタデータは部分的にパース

成熟度レベル:

  • — プロンプトが判断を担い、ツールがコントラクトを担い、ワークフローは薄いライフサイクルグルー。
  • — パターンに部分的に準拠。一部パースまたはフォーマット指示が残存。
  • — 判断、パース、オーケストレーションがワークフローに混在。