Toolは、手順が呼び出せる最小の再利用単位です。型付きのI/Oコントラクトを持つ、決定論的な作業単位です。Toolのオーサリングは意図的に小さく保たれています。入力、出力、使用を許可された効果、そして run 関数を記述します。それ以外のすべて、つまりwireの型チェック、ランタイム検証、挙動を固定するcontent digestは、自動的に導出されます。

Toolのコントラクト

Tool は、ToolContract(そのインターフェース)と run 実装を組み合わせます。run(input, ctx) の組に対して純粋です。すべての効果は ctx に注入されたcapabilityを通じて流れ、ambientな経路は使いません。そして contract.output に一致する値を返します。
export interface Tool<I = JsonValue, O = JsonValue> {
  readonly contract: ToolContract;
  /**
   * Pure with respect to (input, ctx). All effects go through ctx's injected
   * capabilities — never ambient. Returns a value matching contract.output.
   */
  run(input: I, ctx: ToolCtx): Promise<O>;
}

ToolCtx

Riderは runToolCtx を渡します。重要なのは、何が任意なのかです。netfsbrowsersecrets は、コントラクトが対応するcapabilityを宣言した場合にのみ存在します。常に存在するメンバーである clockrandomenvidempotencyKeyloggersignal は、すべてのToolが使用できる決定論的な基盤です。
export interface ToolCtx {
  readonly net?: NetCapability;       // present iff contract declares "net"
  readonly fs?: FsCapability;
  readonly browser?: BrowserCapability;
  readonly clock: Clock;
  readonly random: Random;
  readonly env: EnvView;
  readonly secrets?: SecretsCapability; // present iff contract declares "secrets"
  readonly idempotencyKey: string;
  readonly logger: Logger;
  readonly signal: AbortSignal;
}

2つの不変条件

コントラクトが持つ2つの性質によって、Toolは合成可能かつ再現可能になります。

Capability-gated

capabilityは、コントラクトがそれを宣言した場合にのみ ctx 上に存在します。transform Toolは文字通りネットワークへ到達できません。呼び出すべき ctx.net が存在しないためです。これは原則 T2 であり、構築上強制されます。

型付きI/O

contract.inputscontract.output はスキーマです。コンパイラはオーサリング時にすべてのwireを型チェックし、Riderは実行境界で検証します。
capabilityの不変条件は、安全性を超えた価値を生みます。純粋なToolには効果がないことを証明できるため、エンジンはどの手順を自由に再実行でき、どの手順をJournalへ記録する必要があるかを把握できます。標準的な daily-price-watch Flowでは、スクレイピング結果を整形する transform 手順には ctx.net がないため、純粋で自由に再実行できます。一方で、browser Toolの search-price@1.2.0 と、シークレットなwebhookへの条件付き http.post はcapabilityを保持し、その効果が記録されます。
capabilityはランタイムで手を伸ばして取得するものではありません。宣言するものです。run 本体が ctx.net を必要としているのに capabilities"net" を列挙していない場合、そのフィールドは単に存在せず、呼び出しは失敗します。使用する効果を過不足なく宣言してください。

Toolはファクトリである

Toolはファクトリとして作成されます。createHttpGet(): Tool のような関数がオブジェクトを返します。クラスはなく、共有される可変状態もありません。これによりToolは、どこでも同一に、安全にインスタンス化して実行でき、呼び出し間で何も漏れません。

slugifyの例

コントラクトは十分に小さいため、有用なToolも数行で書けます。capabilityを宣言しない、純粋な transform Toolの例です。
import { z } from "zod";
import { defineTool } from "@browserflow/core";

export const slugify = defineTool({
  name: "slugify",
  version: "1.0.0",
  kind: "transform",
  description: "Lowercase, hyphenate, strip punctuation.",
  inputs: { text: z.string() },          // → InputDecl[] + a typed input schema
  output: z.string(),
  capabilities: [],                        // pure
  run: async ({ text }) => text.toLowerCase().replace(/[^\w]+/g, "-").replace(/^-|-$/g, ""),
});

defineToolはコントラクトを導出する

defineTool は、Zodスキーマから ToolContract を導出します。これには InputDecl[] と、コントラクトのcontent digestが含まれるため、データ型とコントラクトがドリフトすることはありません。これがこのプロジェクトの標準スタイルです。データ型はZodで表し、静的型は z.infer で復元します。スキーマは一度だけ書きます。run に渡される型付き入力、境界での検証、そしてToolのアイデンティティを固定するコントラクトは、すべて同じソースから得られます。 具体的には、上記の inputs マップは、システムの他の部分が推論対象にする宣言済みスロットの配列になります。
/**
 * One declared input slot on a tool. `secret: true` slots never leave the run
 * boundary. `schema` is the JSON-Schema-subset type of the slot.
 */
export const InputDeclSchema = z
  .object({
    name: z.string().min(1),
    schema: JsonSchemaSchema,
    secret: z.boolean(),
  })
  .strict();
transform Toolは純粋なJSON-to-JSON関数です。最も作成しやすく、エンジンにとっても最適化しやすい種類です。記録する代わりに常に再実行できるためです。データの形を変えるだけなら、これを使ってください。

Toolの種類

すべてのコントラクトは1つの kind を宣言します。kindは、コンパイラとRiderに対してToolの形を伝えます。たとえば、デフォルトのリトライポリシーはkindから導出されます。
Kind内容
browser決定論的なPlaywrightオートメーション。一度記録され、モデルなしでリプレイされます。中心的なkindです。
httpHTTPリクエストTool(net、さらにシークレット入力がバインドされている場合は secrets)。
shellコマンド呼び出し(spawn、必要に応じて fs)。
transform純粋なJSON-to-JSON関数。capabilityはありません。
codecapabilityによってサンドボックス化されたescape hatchです。
model宣言され、記録されるLLM呼び出し。オーサリング時の利便性であり、ランタイム上の判断ではありません。
compositeToolとして再公開されたFlow。Flowをネストする仕組みです。
各kindのfirst-party Toolについては 標準Pack を、search-price@1.2.0 の記録とリプレイについては ブラウザーTool を参照してください。

コントラクトdigest

Toolのコントラクトdigestは、その inputsoutput、または capabilities が変わるたびに変化します。Flowは各Tool依存関係を name@version refと解決済みの contract_digest の両方で固定するため、Flowのアイデンティティは単なるバージョンラベルではなく、Toolの挙動を閉じ込めます。
/**
 * A pinned tool dependency: an exact name@version ref AND the resolved
 * contract digest, so a flow's identity closes over tool behavior.
 */
export const RequireSchema = z
  .object({ ref: z.string().min(1), contract_digest: z.string().min(1) })
  .strict();
得られる効果は機械的です。互換性のない変更がパッチに見せかけることはできません。誰かが slugify1.0.0 から 1.0.1 に上げながら、こっそり output スキーマを変えた場合、コントラクトdigestは、それを要求するどのFlowにも固定されているdigestと一致しなくなります。その不一致は、ランタイムではなくコンパイル時に表面化します。
一部の標準Toolは、効果を持つ入力がそれを要求する場合にのみcapabilityを宣言します。std/http は、シークレットを含む入力(認証情報付きURL、authヘッダー、トークン本文)がバインドされたときだけ、正確に secrets を追加します。これにより、http.post に流れ込む secret. 参照は、secret_leak を発生させるのではなく境界ルールを満たします。シークレット入力がバインドされていない場合、httpnet のみです。自分のToolも同じ原則に従います。Toolが実際に使う場合にcapabilityを宣言してください。
secret: true と宣言されたスロットは、FlowSpec、Journal、ログ行、公開済みPackのいずれにも現れません。secrets capabilityを持つToolが、ctx に注入された secrets providerを通じて解決する瞬間まで、これは参照として運ばれます。その解決を行うToolは効果を記録する層でもあるため、何かが書き込まれる前にシークレットをredactする層でもあります。シークレット — 境界 を参照してください。
はい。それが composite kindです。composite Toolは内側のFlowからコントラクトを継承します。宣言される capabilities は、内側のFlowが呼び出すすべてのToolのcapabilityの和集合です(ネストは効果を隠しません)。また、外側のcompositeにある secret: true 入力は、参照として内側のFlowの対応するシークレット入力へ渡されるため、どのネスト深度でも境界は保たれます。

関連

スキーマリファレンス

完全な ToolContractInputDeclRequire スキーマを1か所にまとめています。

標準Pack

first-partyのリファレンスToolです。std/httpstd/transformstd/browser などがあります。

Pack — デプロイ可能なSDK

Toolを、バージョン付きのcontent-addressed artifactにバンドルして公開します。