AI & Agents13 min readMarian Engineering

エージェントAPIに「責任境界」を埋め込む — エンベロープ・プロトコルの設計

trace ID、座標ルーティング、5段階の権限判定をすべてのAPIルートに一律適用する

Key Takeaways

  • 全API入出力を共通メタデータ付きの封筒で包み、trace・発信元・権限を構造的に強制する
  • G.U.P.Z.A形式の座標がエージェントの住所になり、集計・ポリシー適用の単位として機能する
  • 責任境界はblocked/avoid/suggest_only/reconfirm/allowedの5段階で、can と should のずれを表現する
  • 規約はレジストリ+withEnvelopeアダプタ+静的ハーネスの3点セットで機械的に守らせる
  • 生JSONも受け付ける後方互換(受信は寛容に、送信は厳格に)で段階移行する

課題: エージェントAPIは「ただのJSON」では追跡できない

エージェントが増えると、API呼び出しの様相が変わります。人間のクライアント1種類ではなく、エージェントがエージェントを呼び、その結果がさらに別のエージェントに渡る。このとき素のJSONボディだけだと、(1)リクエストの連鎖を横断するtraceが取れない、(2)「誰が」呼んだかの語彙がない、(3)エージェントごとの権限の宣言場所がない、という3つの欠落が運用を直撃します。

Marianの答えは、全APIの入出力をエンベロープ(封筒)で包むことです。中身(payload)が何であれ、封筒には必ず同じメタデータが書いてあります。

// lib/marian-protocol/envelope.ts — marian.envelope.v1
RequestEnvelope<T>  = { kind: 'request',  meta: EnvelopeMeta, payload: T }
ResponseEnvelope<T> = { kind: 'response', meta: EnvelopeMeta,
                        status: 'ok' | 'error' | 'rejected',
                        payload: T | null,
                        error: { code, message, retryable? } | null,
                        boundary: 'blocked' | null }

EnvelopeMeta = {
  protocol: 'marian.envelope.v1',
  id: string,          // env_* 封筒ごとに一意
  traceId: string,     // trace_* リクエスト→レスポンス→次のホップを貫通
  issuedAt: string,
  source: string,      // 例: 'marian-client'
  target: string,      // ルートid 例: 'marian-ai'
  coordinate: string,  // 例: 'G1.U1.P2.Z1.A1'
  capability: string,  // 例: 'ai:chat'
}

座標系: エージェントに住所を与える

coordinateG{galaxy}.U{universe}.P{planet}.Z{zone}.A{agent}形式の階層アドレスです。Planetがプロダクト領域(P1=knowledge、P2=ai、P3=ingest、P5=agents、P9=memory…)、Zoneがケイパビリティ群、Agentが個々のハンドラに対応します。現在13のAPIサーフェスがレジストリに登録されています。

surfacecoordinatecapability
askG1.U1.P1.Z1.A1ask:query
ask-indexG1.U1.P1.Z1.A2ask:index
marian-aiG1.U1.P2.Z1.A1ai:chat
marian-ingestG1.U1.P3.Z1.A1ingest:source
marian-agentsG1.U1.P5.Z1.A1agents:run
marian-memoryG1.U1.P9.Z1.A1memory:read

UUIDでもURLパスでもなく構造化座標にする利点は、集計とポリシーの単位が住所から読めることです。「P3(ingest)配下の全エージェントのエラー率」「Z1ゾーンに新しい権限を一括付与」のような操作が、命名規約だけで可能になります。

5段階の責任境界

このプロトコルの中心は、各エージェントが宣言する責任エンベロープです。能力(できること)と権限(してよいこと)を分離し、グレーゾーンを2値ではなく5段階で表現します。

ResponsibilityEnvelope = {
  agentId, coordinate, role,
  grantedCapabilities: string[],  // 付与された能力
  naturalAvoidances: string[],    // 技術的には可能だが姿勢として避ける
  suggestOnly: string[],          // 起案はするが決定は人間
  reconfirmBefore: string[],      // 実行前に平易な言葉で再確認
  hardBoundaries: string[],       // 絶対禁止
}

// 判定の優先順位(上から評価)
hardBoundary or 未付与capability → boundary: 'blocked',     emission: 'none'
naturalAvoidance                → boundary: 'avoid',       emission: 'reconfirmation_request'
suggestOnly                     → boundary: 'suggest_only', emission: 'proposal'
reconfirmBefore                 → boundary: 'reconfirm',   emission: 'reconfirmation_request'
それ以外                         → boundary: 'allowed',     emission: 'execute'

注目すべきはblockedallowedの間にある3段階です。リサーチ系エージェントなら「出典の捏造」はhard boundary、「最終結論を出す」はsuggest only、「外部送信・公開」はreconfirm。「technically can(技術的に可能)」と「should(すべき)」のずれを、コードに書ける語彙にしたものです。判定結果はレスポンス封筒のboundaryフィールドとstatus: rejectedで呼び出し側に伝わるため、拒否もまた構造化データになります。

withEnvelope: ルート実装をボイラープレートから解放する

Next.jsのRoute HandlerはwithEnvelopeアダプタで包みます。封筒の解析・検証・trace付与・エラー整形はアダプタが行い、ハンドラはpayloadとevaluate(境界判定関数)だけを受け取ります。

export const POST = withEnvelope<AskRequest, AskResult>('ask', async ({ payload, evaluate }) => {
  if (!payload.question) throw new EnvelopeHttpError(400, 'missing_question', 'Question required.')
  // evaluate(action) → { boundary, permittedEmission, reason }
  return runAsk(payload)
})

ストリーミング応答にはwithEnvelopeStreamという変種があります。SSE/NDJSONのボディはそのまま流し、封筒メタデータはx-marian-tracex-marian-coordinatex-marian-capabilityヘッダに載せ替えます。ストリーム開始前のエラーだけJSON封筒で返す、という折衷で「全応答が封筒を持つ」不変条件をストリーミングでも維持します。

後方互換も重要な設計判断です。unwrapPayload()は生のJSONボディも封筒入りボディも受け付けるため、既存クライアントは壊れず、レスポンスは常に封筒付きで返ります。移行は「受信は寛容に、送信は厳格に」の一方通行で進みます。

プロトコルは書くだけでは守られない — 静的検証ハーネス

この種の規約の最大の敵は「新しいルートが規約を知らずに生える」ことです。Marianには2つの静的ハーネスがあり、CIでルート実装とレジストリを突き合わせます。

  • APIコントラクトハーネス: 全route.tsを走査し、未登録サーフェス、withEnvelope未採用、try/catchなしの.json()読み、検証なしのボディ読みなどを検出。error -0.6 / warning -0.15のペナルティでスコア化
  • エンベロープハーネス: レジストリ側を検証。座標の重複・パース不能、capability未付与、責任境界4配列(avoidances/suggestOnly/reconfirmBefore/hardBoundaries)の欠落を検出
プロトコルの実体はドキュメントではなく、レジストリ(唯一の真実)+アダプタ(実装の強制)+ハーネス(逸脱の検出)の3点セットである。

まとめ

エンベロープ・プロトコルは、(1)全API入出力に共通メタデータ(trace・座標・capability)を強制する封筒、(2)blocked/avoid/suggest_only/reconfirm/allowedの5段階責任境界、(3)withEnvelope/withEnvelopeStreamによる実装の一律化、(4)静的ハーネスによる規約の機械的検証、で構成されます。エージェントが増えるほど、この「全員が同じ封筒を使う」制約が可観測性とガバナンスの土台になります。

FAQ

エンベロープ・プロトコルは何を解決するのか?
マルチエージェント環境での(1)リクエスト連鎖を貫通するtrace、(2)発信元・宛先・権限の標準語彙、(3)エージェントごとの責任境界の宣言場所、の3つの欠落を解決します。全APIの入出力がprotocol/id/traceId/source/target/coordinate/capabilityを持つ封筒で包まれ、拒否応答もboundary付きの構造化データとして返ります。
なぜ権限を許可/禁止の2値ではなく5段階にするのか?
エージェントの行動には「技術的には可能だが姿勢として避ける(avoid)」「起案はするが決定は人間(suggest_only)」「実行前に平易な言葉で再確認(reconfirm)」というグレーゾーンが本質的に存在するためです。2値ではこれらをすべてblockedに倒すか、危険を許可するかの二択になります。5段階なら判定ロジックが優先順位つきの決定木として実装できます。
ストリーミング応答でもエンベロープは維持されるのか?
維持されます。withEnvelopeStreamがボディ(SSE/NDJSON)はそのまま流し、封筒メタデータをx-marian-trace、x-marian-coordinate、x-marian-capabilityヘッダに載せます。ストリーム開始前に起きたエラーのみ通常のJSON封筒で返すため、「全応答が封筒情報を持つ」という不変条件が保たれます。