アクションは、必要な機能が揃ったサーバー側ツールです。素の AI SDK tool() が説明、スキーマ、execute 関数だけなのに対し、action() は、実際の副作用を持つツールで手作業だと面倒で危険なものを足します。
- べき等性 — 耐久台帳が安定キーで確定結果を再生し、復旧再試行で副作用を再実行しません。
- 承認 — 人手の後ろに呼び出しを置きます。インライン(ターンが待つ)か、耐久的(ターンを駐車し、ライブソケットのないダッシュボードからでもあとで再開)です。
- 認可 — 呼び出しが必要とする権限を宣言し、ターンごとに付与します。
- 返信添付 — モデルが見る内容は変えず、助言的な配送メタデータ(下書きメール、カード、ボイスノート)を記録します。
アクションは Think ツールにコンパイルされるので、モデルは他のツールと同じように呼び出します。getActions() から返します。Think は getTools()、ワークスペースツール、拡張、MCP ツールと一緒にツールセットへマージします。
action() 記述子ファクトリを使い、getActions() からアクションのマップを返します。マップキーは、モデルが見るツール名です(name を設定した場合を除く)。execute の入力型は inputSchema から推論されます。
import { Think, action } from "@cloudflare/think";
import { z } from "zod";
export class Support extends Think {
getActions() {
return {
refundOrder: action({
description: "Refund a customer order.",
inputSchema: z.object({
orderId: z.string(),
amountCents: z.number().int().positive(),
}),
execute: async ({ orderId, amountCents }, ctx) => {
const result = await refund(orderId, amountCents);
return { refundId: result.id, status: result.status };
},
}),
};
}
}import { Think, action } from "@cloudflare/think";
import { z } from "zod";
export class Support extends Think<Env> {
getActions() {
return {
refundOrder: action({
description: "Refund a customer order.",
inputSchema: z.object({
orderId: z.string(),
amountCents: z.number().int().positive(),
}),
execute: async ({ orderId, amountCents }, ctx) => {
const result = await refund(orderId, amountCents);
return { refundId: result.id, status: result.status };
},
}),
};
}
}execute コールバックは、検証済み入力と ActionContext を受け取ります。
type ActionContext = {
agent: Think;
env: Cloudflare.Env;
requestId: string;
toolCallId: string;
messages: ReadonlyArray<ModelMessage>;
signal: AbortSignal; // aborts on turn cancel or after `timeoutMs`
attachReply(attachment: ReplyAttachment): void;
};アクション出力は JSON に正規化され、モデルへ見せる前に切り詰められます(長い出力は上限があります)。execute から投げたものはターンを落とさず、構造化 { error: { name, message } } のツール結果になります。各アクションのデフォルトタイムアウトは 30 秒です。timeoutMs でアクションごとに上書きします。
getTools() の素の tool() も使えます。読み取り専用や単純なツールにはそれが適切です。二度実行してはいけない副作用がある、人手承認が要る、宣言的な認可が要る、といったときに action() を使います。台帳、承認記述子、デフォルトタイムアウト、構造化エラー対応はアクションにだけ適用されます。
アクションが idempotencyKey を宣言すると、Think は確定結果を action:<name>:<key> をキーにした耐久台帳に記録します。同じキーが再び見えた場合(復旧再試行、再接続、重複した受信イベント)、Think は execute を再実行せず保存済み結果を返すので、正常系では副作用は最大 1 回です。
const chargeInvoice = action({
description: "Charge an invoice.",
inputSchema: z.object({ invoiceId: z.string() }),
// Use a stable domain identifier — never a timestamp, request id, or random value.
idempotencyKey: ({ input }) => `invoice:${input.invoiceId}`,
execute: async ({ invoiceId }) => charge(invoiceId),
});const chargeInvoice = action({
description: "Charge an invoice.",
inputSchema: z.object({ invoiceId: z.string() }),
// Use a stable domain identifier — never a timestamp, request id, or random value.
idempotencyKey: ({ input }) => `invoice:${input.invoiceId}`,
execute: async ({ invoiceId }) => charge(invoiceId),
});idempotencyKey は文字列、または ({ input, ctx }) => string 関数です。復旧再試行をまたいで残るキー(注文 ID、受信イベント ID)を選び、試行ごとに変わる値は使いません。idempotencyKey がないアクションは、toolCallId ごとのキーにフォールバックします。同じツール呼び出し内の重複排除だけです。再試行横断ではありません。
台帳行は execute の前に pending で書き込まれ、成功時に settled へ変わります(投げられた、またはタイムアウトした execute は行を削除し、きれいに再試行できます)。アイソレートが実行途中で落ちると、行は pending のままです。デフォルトでは、その古い行は回収され、行が actionLedgerPendingRetryLeaseMs(デフォルト 5 分)より古くなるとアクションが再実行されます。ただし明示的な idempotencyKey を宣言したアクションだけです。そのキーは、キー付き副作用の再実行が安全だという表明だからです。新しい pending 行(または明示キーのない行)は、代わりに ActionPendingError 結果を返し、モデルが未知状態を盲目再試行しないようにします。actionLedgerPendingRetryLeaseMs = false にすると回収を完全に止め、古い行には常に ActionPendingError を出します。
approval でアクションを人手の後ろに置きます。メカニズムは 2 つあり、kind で選びます。
kind なしで approval を設定したときのデフォルトです。アクションは AI SDK の needsApproval フラグ付きツールにコンパイルされます。ストリームは approval-requested パートで一時停止し、クライアントが承認または拒否し、ターンはインラインで続きます。execute は承認後にだけ走ります。
const deleteAccount = action({
description: "Permanently delete a user account.",
inputSchema: z.object({ userId: z.string() }),
approval: true, // or ({ input }) => input.userId !== currentUser
approvalSummary: "Delete an account",
approvalRisk: "high",
execute: async ({ userId }) => deleteAccount(userId),
});const deleteAccount = action({
description: "Permanently delete a user account.",
inputSchema: z.object({ userId: z.string() }),
approval: true, // or ({ input }) => input.userId !== currentUser
approvalSummary: "Delete an account",
approvalRisk: "high",
execute: async ({ userId }) => deleteAccount(userId),
});approval は boolean、または述語 ({ input, ctx }) => boolean です。リスクの高い入力だけ承認を要求できます。approvalSummary と approvalRisk("low" | "medium" | "high")は、UI が描画する承認記述子に入ります。
承認に数分から数日かかり、接続を開き続けたくないときは kind: "durable-pause" を設定します。アクションは耐久ストアに駐車され、ターンは終了します。execute はまだ走りません。あとでどこからでも(ライブ WebSocket のないダッシュボードを含む)approveExecution() または rejectExecution() で再開します。
const deploy = action({
description: "Deploy to production.",
inputSchema: z.object({ ref: z.string() }),
kind: "durable-pause",
approvalSummary: "Deploy to production",
approvalRisk: "high",
permissions: ["deploy:run"],
execute: async ({ ref }) => deploy(ref),
});const deploy = action({
description: "Deploy to production.",
inputSchema: z.object({ ref: z.string() }),
kind: "durable-pause",
approvalSummary: "Deploy to production",
approvalRisk: "high",
permissions: ["deploy:run"],
execute: async ({ ref }) => deploy(ref),
});// List everything waiting on a human (cold-load reconciliation):
const pending = await agent.pendingApprovals();
// [{ executionId, source: "action" | "codemode", descriptor }]
// Approve or reject by execution id (idempotent — a second call is a no-op):
await agent.approveExecution(executionId);
await agent.rejectExecution(executionId, "Not this release");// List everything waiting on a human (cold-load reconciliation):
const pending = await agent.pendingApprovals();
// [{ executionId, source: "action" | "codemode", descriptor }]
// Approve or reject by execution id (idempotent — a second call is a no-op):
await agent.approveExecution(executionId);
await agent.rejectExecution(executionId, "Not this release");approveExecution() は execute を一度実行し、クライアント未接続でもターンを自動継続します。rejectExecution() は実行せずにアクションを解決します。pendingApprovals() は駐車中のアクションと一時停止中の Codemode 実行をマージするので、1 つの承認 UI で両方を扱えます。(durable-pause には approval ポリシーが必要です。駐車しないアクションは定義時に拒否されます。)
承認ゲートと耐久一時停止のパートは、どちらも安定した ActionApprovalDescriptor({ requestId, toolCallId, action, summary, input, permissions, risk, kind })を運ぶので、UI はプロンプト描画に必要な情報をすべて持てます。
permissions でアクションが必要とする権限を宣言し、ターンごとに付与します。デフォルトでは毎ターン完全認可なので、認可はオプトインです。
const refundOrder = action({
description: "Refund a customer order.",
inputSchema: z.object({ orderId: z.string() }),
permissions: ["billing:refund"], // or ({ input }) => [...]
execute: async ({ orderId }) => refund(orderId),
});const refundOrder = action({
description: "Refund a customer order.",
inputSchema: z.object({ orderId: z.string() }),
permissions: ["billing:refund"], // or ({ input }) => [...]
execute: async ({ orderId }) => refund(orderId),
});authorizeTurn() をオーバーライドし、ターンごとに一度、付与する権限を決めます。リストを返すと付与が狭まります。セット外の権限を必要とするアクションは、構造化 ActionAuthorizationError で拒否されます(モデルは execute を呼びません)。
export class Support extends Think {
authorizeTurn(ctx) {
const role = ctx.body?.role;
if (role === "admin") return true; // full grant (the default)
return { allowed: true, grantedPermissions: ["billing:read"] };
}
}export class Support extends Think<Env> {
override authorizeTurn(ctx: TurnContext): ActionAuthorizationDecision {
const role = (ctx.body as { role?: string })?.role;
if (role === "admin") return true; // full grant (the default)
return { allowed: true, grantedPermissions: ["billing:read"] };
}
}authorizeTurn() は true(完全付与)、false(すべて拒否)、または { allowed, reason?, grantedPermissions? } を返します。呼び出しごとのロジックには、代わりに authorizeAction(ctx) をオーバーライドします。アクション名、kind、入力、必要な権限と付与済み権限を受け取ります。
アクションは ctx.attachReply() で、ターン向けの助言的な配送メタデータ(下書きメール、カード、ボイスノート)を記録できます。添付はモデルが見るツール出力を変えません。配送層が描画するために応答に添えます。
const draftReply = action({
description: "Draft an email reply.",
inputSchema: z.object({ to: z.string(), subject: z.string() }),
execute: async ({ to, subject }, ctx) => {
ctx.attachReply({ type: "email_draft", to: [to], subject });
return { drafted: true };
},
});const draftReply = action({
description: "Draft an email reply.",
inputSchema: z.object({ to: z.string(), subject: z.string() }),
execute: async ({ to, subject }, ctx) => {
ctx.attachReply({ type: "email_draft", to: [to], subject });
return { drafted: true };
},
});ターン後に添付を読むには、onChatResponse() フック、または replyAttachments(requestId?) ゲッターを使います。
export class Support extends Think {
async onChatResponse(result) {
for (const attachment of result.attachments ?? []) {
// attachment.type === "email_draft" | "card" | "voice_note" | custom
}
}
}export class Support extends Think<Env> {
override async onChatResponse(result: ChatResponseResult) {
for (const attachment of result.attachments ?? []) {
// attachment.type === "email_draft" | "card" | "voice_note" | custom
}
}
}添付は JSON 正規化され、読み取り時にディープコピーされ、ターンごとに上限があり、記録した execute が失敗すると破棄されます。台帳再生は添付を再発火しません(副作用はすでに起きています)。permissions、approval、idempotencyKey コールバックからの attachReply() は no-op です。添付は execute から記録します。
組み込みの ReplyAttachment は email_draft、card、voice_note をカバーします。カスタム配送には任意の { type: string; ... } 形状が使えます。添付をチャネル通知にするには renderAttachment() をオーバーライドします。
| フィールド | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
description |
string |
はい | — | モデルに見せるツール説明です。 |
inputSchema |
FlexibleSchema(Zod、Valibot、または AI SDK jsonSchema) |
はい | — | execute 入力を検証し、型付けします。 |
execute |
(input, ctx) => Output | Promise<Output> |
はい | — | アクション本体です。検証済み入力と ActionContext を受け取ります。 |
name |
string |
いいえ | マップキー | ツール名を上書きします。 |
idempotencyKey |
string | ({ input, ctx }) => string |
いいえ | ツール呼び出しごと | 台帳再生用の安定キーです。ドメイン識別子を使います。 |
permissions |
readonly string[] | ({ input, ctx }) => readonly string[] |
いいえ | なし | この呼び出しが必要とする権限です(認可を参照)。 |
approval |
boolean | ({ input, ctx }) => boolean |
いいえ | なし | 呼び出しを人手の後ろに置きます。 |
approvalSummary |
string |
いいえ | description |
承認記述子内の人が読める要約です。 |
approvalRisk |
"low" | "medium" | "high" |
いいえ | — | 承認記述子内のリスクヒントです。 |
kind |
"server" | "approval-gated" | "durable-pause" |
いいえ | 推論 | approval があるときは approval-gated、なければ server。durable-pause は明示設定します。 |
timeoutMs |
number |
いいえ | 30000 |
アクションごとの実行タイムアウトです(ctx.signal も駆動します)。 |
| メンバー | 説明 |
|---|---|
getActions() |
ツールにコンパイルするアクション記述子を返します。 |
authorizeTurn(ctx) |
ターンごとに一度、付与する権限を決めます。デフォルトは完全付与です。 |
authorizeAction(ctx) |
アクション呼び出しごとに認可を決めます。デフォルトは authorizeTurn の付与を確認します。 |
pendingApprovals(executionId?) |
承認待ちの駐車アクションと一時停止中の Codemode 実行を一覧します。 |
approveExecution(executionId) |
駐車中の実行を承認します。execute を走らせ、ターンを自動継続します。 |
rejectExecution(executionId, reason?) |
実行せずに駐車中の実行を拒否します。 |
replyAttachments(requestId?) |
ターン中に記録した助言的添付を読みます。 |
actionLedgerPendingRetryLeaseMs |
古い pending の回収窓です(デフォルト 300000。無効化は false)。 |
- ツール — ワークスペースツール、コード実行、拡張です。
- Human in the loop — 承認フローの全体です。
- チャネル — 添付と帯域外通知の配送です。