news 2026/9/17 22:51:23

Cloudflare Think 生产级 Actions 实践指南:用声明式 `action()` 构建带权限、审批、幂等与恢复保障的 Agent 工具

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Cloudflare Think 生产级 Actions 实践指南:用声明式 `action()` 构建带权限、审批、幂等与恢复保障的 Agent 工具

Cloudflare Think 生产级 Actions 实践指南:用声明式action()构建带权限、审批、幂等与恢复保障的 Agent 工具

【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents

本指南以 Cloudflare Agents 框架中@cloudflare/think包的 Think Actions 设计为骨架,系统讲解如何用action()包装器把普通 AI SDK 工具升级为携带权限、审批策略、幂等键、超时与恢复分类等生产级元数据的"动作"。阅读后你将掌握getActions()注册、授权钩子、审批暂停与恢复、账本防重放、ctx.attachReply交付元数据等一整套可落地的实现方案,并能直接在现有getTools()/beforeToolCall/afterToolCall架构之上增量接入。

背景:为什么 Think 需要一套"生产级工具"

在 packages/think/src/think.ts 中,Think 的工具就是getTools(): ToolSet返回的普通 AI SDK 工具。它们可用,但一个生产级动作所需要的一切都散落在应用层各自实现,成为"生产语义靠民间传说"(production semantics as folklore)的困境:

  • 权限/授权:没有声明式手段表达"此工具需要billing:refund",唯一的拦截点是命令式的beforeToolCall钩子,每个应用都要手写。
  • 审批:存在两套各自为政的审批机制(AI SDKneedsApproval与 execute/codemode 的 durable-pause 路径),但没有一个稳定的审批描述符让 Web、语音、Messenger 各端一致渲染。
  • 幂等think.ts中确认没有按工具调用粒度的执行账本;幂等只存在于 submission、messenger 事件和 transcript 层。副作用工具执行后、结果在持久化前崩溃,恢复时就会重复执行。
  • 护栏:没有通用的单工具超时、结构化工具错误信封和输出截断,这些保护只存在于 workspace bash、execute 沙箱等特定工具内部。
  • 交付影响:工具无法告诉渠道"把这条回复作为语音/邮件草稿/卡片发送",Messenger 交付层只保留text-delta

设计目标:新增action()包装器编译成 AI SDK 工具但携带声明式元数据;新增持久化动作账本让已结算的服务器动作在恢复时防重放;新增稳定的审批描述符复用现有审批/HITL 机制;提供默认护栏与ctx.attachReply侧信道;全程增量兼容——getTools()beforeToolCall/afterToolCall行为不变。

1.action()描述符:从工具到动作

action()返回一个带品牌标记(branded)的Action描述符,对输入输出泛型化,输入类型从 schema 推断,execute全程类型安全、无需代码生成。

1.1 类型定义与五种 ActionKind

type ActionKind = | "server" // 服务端执行;受账本保护;默认 | "client" // 客户端解析(编译为客户端工具) | "approval-gated" // 执行前需审批(AI SDK needsApproval 路径) | "durable-pause" // 长时间运行;暂停回合(execute/codemode 路径) | "delegated-agent"; // 委派给子代理(agent-tool 路径) interface ActionConfig<Input, Output> { /** 通过 getActions() 注册时默认取注册键。 */ name?: string; description: string; /** Zod(或 AI SDK)schema;输入类型从中推断。 */ inputSchema: StandardSchemaV1<Input> | ZodType<Input>; outputSchema?: StandardSchemaV1<Output> | ZodType<Output>; /** 声明式权限范围,运行所需。 */ permissions?: | string[] | ((args: { input: Input; ctx: ActionContext }) => string[]); /** * 审批策略。true 总是要求审批;谓词按输入逐次决定。 * "approval-gated" 编译为 AI SDK needsApproval;长时间运行的服务端工作 * 编译到 durable-pause 路径。 */ approval?: | boolean | ((args: { input: Input; ctx: ActionContext; }) => boolean | Promise<boolean>); /** * 账本去重的稳定键。缺省时账本回退到 tool-call id(见"幂等账本")。 * 提供它可跨重试/webhook 去重(如 `refund:${input.paymentId}:${input.amount}`)。 */ idempotencyKey?: string | ((input: Input) => string); /** 每动作超时。默认按 kind 施加(见"护栏")。 */ timeoutMs?: number; /** 显式 kind;否则自动推断(见"恢复分类法")。 */ kind?: ActionKind; execute(input: Input, ctx: ActionContext): Promise<Output> | Output; }

Action描述符是一个冻结对象,带Symbol.for("cf.think.action")品牌键,让getActions()/getTools()的糖可以区分Action与普通 AI SDK 工具。源码证据位于 packages/think/src/think.ts:ACTION_BRAND常量、ActionKind联合类型、ActionConfig/Action接口、action()工厂(对 config 做Object.freeze({ ...config }))以及isAction()类型守卫。

实现细节:当前实现包含namedescription、schema 推断的inputSchema类型、outputSchema(保留元数据)、permissionstimeoutMskindapprovalapprovalSummaryapprovalRiskexecuteidempotencyKey曾计划到账本落地后生效,如今已随账本一并落地。

1.2 一个完整的动作示例

const refundPayment = action({ description: "Refund a payment", inputSchema: z.object({ paymentId: z.string(), amount: z.number() }), permissions: ["billing:refund"], approval: ({ input }) => input.amount > 100, idempotencyKey: ({ paymentId, amount }) => `refund:${paymentId}:${amount}`, timeoutMs: 15_000, async execute(input, ctx) { return ctx.env.BILLING.refund(input.paymentId, input.amount); } });
  • permissions: ["billing:refund"]声明运行所需的权限范围;
  • approval谓词决定"金额超过 100 才需要审批";
  • idempotencyKey从业务数据推导稳定键,跨重试去重;
  • timeoutMs: 15_000施加单动作超时;
  • execute(input, ctx)通过ctx.env访问绑定,ctx是完整执行上下文。

编程错误防护action()会拒绝kind: "durable-pause"搭配approval: false的组合——一个永不暂停的 durable-pause 是编程错误(源码见 packages/think/src/think.ts)。

2. 注册与编译:getActions()与编译执行管线

getTools()旁边新增一个异步钩子:

getActions(): Record<string, Action> | Promise<Record<string, Action>> { return {}; }

(允许返回Promise,与其他异步注册钩子对齐——动作集合可能依赖env/远程配置。)

在推理循环_runInferenceLoop中,动作在getTools()之后被转换并合并进工具集(后面的层仍然胜出,与现有合并顺序一致):

const actionTools = mapValues(this.getActions(), (a, name) => actionToTool(a, name, this) ); const tools = { ...workspaceTools, ...baseTools, // getTools() ...actionTools, // getActions() <-- 新增 ...extensionTools, ...contextTools, ...skillTools, ...(this.mcp?.getAITools?.() ?? {}), ...clientToolSet };

actionToTool(action)产出一个普通 AI SDK 工具,其execute是"护栏化/授权化/账本化"的管线,needsApprovalapproval策略推导。因为结果是普通工具,它会流经现有_wrapToolsWithDecision路径,所以beforeToolCall/afterToolCall照常触发——beforeToolCall仍是最外层闸门(可以拦截),动作管线在最内层。

为什么独立getActions()而不是让Action直接进getTools()ToolSet是 AI SDK 类型,Action不是 AI SDK 工具;分开两个钩子让两边类型面保持干净。

2.1 编译执行管线(权威顺序)

actionToTool从外到内组合这些层(第 4–8 节详述各步):

  1. _wrapToolsWithDecisionbeforeToolCall(现有;可拦截/替换,保持最外层闸门);
  2. 授权authorizeAction)。拒绝 → 结构化工具输出,跳过其余步骤。推导needsApproval时也会咨询它,使未授权的 approval-gated 动作永不弹出审批(先授权后审批);
  3. 审批approval-gated经 AI SDKneedsApproval在执行前门控;durable-pause走暂停路径。拒绝 →output-denied
  4. 幂等账本查询(已结算 → 返回存储结果,见 §6);
  5. 超时/中止——ctx.signal(回合信号 + 每动作超时);
  6. execute(input, ctx)
  7. 输出处理——outputSchema校验(如设置)→ 安全序列化 → 截断;
  8. 账本写入(成功settled,抛错failed);
  9. 结构化错误映射(抛错 →output-error,流存活);
  10. afterToolCall(经experimental_onToolCallFinish,观察用)。

needsApproval是 AI SDK 工具选项(boolean | (opts) => boolean | Promise<boolean>);approval策略与授权检查(第 2 步)编译进它(仅approval-gatedkind)。

3.ActionContextctx):工具能得到的一切

ctx是 AI SDK 传给execute的内容({ toolCallId, messages, abortSignal })的超集,外加 Think 生产级便利设施:

interface ActionContext { /** Agent 实例。 */ agent: Think; env: Cloudflare.Env; /** 当前回合的请求 id。 */ requestId: string; toolCallId: string; /** 调用时刻可见的模型消息。 */ messages: ReadonlyArray<ModelMessage>; /** * 组合中止信号:回合信号 + 每动作超时。回合取消或动作超时都会触发。 */ signal: AbortSignal; /** 本回合解析出的授权上下文(见"权限")。 */ authorization: AuthorizationContext; /** 为最终回复附加侧信道交付元数据。 */ attachReply(attachment: ReplyAttachment): void; /** 结构化日志器;发出 action:* 可观测性事件。 */ log: ActionLogger; }

agentenvmessages给动作一切工具所需;signalattachReply是新增能力。

4. 权限与授权:声明"需要什么"与解析"拥有什么"

授权分两半:动作声明所需permissions: string[]或谓词),调用方所持(按回合解析)。

type ActionAuthorizationDecision = | boolean | { allowed: boolean; reason?: string; grantedPermissions?: readonly string[]; }; authorizeTurn(turn: TurnContext): ActionAuthorizationDecision { // 默认:true(完全授权,向后兼容) return true; } authorizeAction(ctx: ActionAuthorizationContext): ActionAuthorizationDecision { // 默认:grantedPermissions 为 undefined => 完全授权;否则要求每个声明的 // 权限都在回合授权中出现。 }

授权来源由应用通过authorizeTurn自持。常见来源:渠道(渠道可声明默认授权——尚未构建;但ChannelContext留有未来grants字段的文档化接缝,可喂给authorizeTurn)、会话或请求body

  • 拒绝时动作不执行,模型收到结构化工具输出({ error: { name: "ActionAuthorizationError", ... } }),助手可解释而非崩溃。
  • 默认行为是完全授权,所以现有应用无感知,直到显式接线。
  • 已落地的实现:authorizeTurn每回合运行一次(在beforeTurn覆盖生效后);默认authorizeAction在提供grantedPermissions时强制校验、否则放行全部;approval-gated 动作在推导needsApproval时咨询授权,被拒动作跳过提示并返回结构化授权错误。
  • 重要permissionsapproval谓词可能在决定是否提示时运行一次、在已批准的调用恢复时再运行一次,因此必须保持纯函数、无副作用

ActionAuthorizationContext的具体字段(requestId/toolCallId/action/kind/input/requiredPermissions/grantedPermissions/messages/agent/env)见 packages/think/src/think.ts。

5. 审批:复用,而非重造

approval编译到两套现有机制而不是新造一套:

  • approval-gated(approval 的默认):在编译工具上设置 AI SDKneedsApproval,走现有 transcript 路径——approval-requestedpart 状态 →tool-approvalWS 事件 →_applyToolApproval/toolApprovalUpdate→ 自动续跑(_scheduleAutoContinuation)。
  • durable-pause(长时间运行的服务端审批):停靠在专用持久化存储cf_think_action_pending_approvals中,经approveExecution/rejectExecution恢复,回合结束、稍后恢复,跨部署存活。暂停执行的 id 以actpause_为前缀(源码常量见 packages/think/src/think.ts),恢复路径据此区分动作暂停与 codemode 暂停;无匹配行时回退到 codemode 运行时。

5.1 稳定审批描述符

两类之上,动作发出统一的稳定描述符,让每个界面渲染一致:

type ActionApprovalDescriptor = { requestId: string; toolCallId: string; action: string; /** 模型/应用提供的将要发生什么的摘要。 */ summary: string; input: unknown; permissions: string[]; /** 给 UI 的粗略风险提示;仅建议性。 */ risk?: "low" | "medium" | "high"; kind: "approval-gated" | "durable-pause"; };
  • 该描述符挂在approval-requestedpart 上并暴露给客户端(Web/语音/Messenger),语音代理可以"念出来",Web UI 可以用同一份数据渲染卡片。
  • 审批描述符的三处挂载:approval-gated 请求挂在part.approval.descriptor上(AI SDK approval 对象,审批/拒绝时由toolApprovalUpdate保留);durable-pause 与 codemode 暂停是已结算的output-availablepart(非 AI SDK approval),描述符挂在兄弟字段part.approvalDescriptor上——因为若挂到part.approval(尚无决策),下一回合convertToModelMessages会对一个已解析的输出发出非法的tool-approval-request

5.2 暂停时授权一次,恢复不重复授权

durable-pause 动作在首次停靠时授权并求值approval谓词;只有被允许继续才写行。批准/恢复不重新授权(此时已没有回合上下文),恢复路径直接运行已批准输入。因此授权决策在暂停时刻固化。

5.3 连接无关的恢复:claim-by-delete

approveExecution/rejectExecutionclaim-by-delete认领暂停行:DELETE ... WHERE execution_id=?并检查changes()==1,使并发 approve/reject/replay 幂等——恰好一个调用者胜出。之后通过共享的_runLedgeredAction管线运行已批准动作,再驱动_applyExecutionOutcome。没有活跃客户端连接时,结果会调度一个自驱动续跑(借鉴 submission/alarm 路径),因此仪表盘无 socket 也能推进聊天回合——这同时修复了 codemode 从仪表盘approveExecution的问题。

源码佐证(packages/think/src/think.ts):_claimActionPendingRow读取后立即删除,SQLite 调用同步且读写之间没有await,因此在单个 DO isolate 内是无竞态的——并发同 id 的 approve/reject 只能在下一次 await 边界执行,此时行已消失(返回 null → "already resolved")。

5.4 冷加载协调、TTL 清理与生命周期事件

  • durable-pause 描述符冷加载时从暂停行重建;codemode 描述符从活跃暂停执行推导,可用可覆盖的describePausedExecution(pending, ctx)钩子丰富(默认undefined)。
  • pendingApprovals(executionId?)把 codemode 与动作暂停行合并成一份列表,每条携带描述符,供冷加载协调。source: "action"表示停靠的 durable-pause 动作,source: "codemode"表示暂停的 execute 工具执行;两者都经approveExecution/rejectExecution解析(见 packages/think/src/think.ts)。
  • 被遗弃的暂停行由_sweepActionPendingApprovals清理(select-then-delete-by-id、节流、可配置actionPendingApprovalTtlMs,默认 30 天,false可禁用,见 packages/think/src/think.ts 与 #L9718),在onStart恢复时接线。
  • 生命周期可观测:action:pause:created/:approved/:rejected/:swept

6. 幂等账本:让已结算的副作用防重放

持久化表让已结算的服务器动作对稳定键防重放:若动作已结算,Think 返回存储的模型可见输出而不是再跑一次execute

CREATE TABLE IF NOT EXISTS cf_think_action_ledger ( key TEXT PRIMARY KEY, -- action:name:key 或 `tool:${toolCallId}` action_name TEXT NOT NULL, request_id TEXT, tool_call_id TEXT, input_hash TEXT NOT NULL, -- 归一化输入的稳定哈希 status TEXT NOT NULL, -- 'pending' | 'settled' result_json TEXT, -- 已结算模型输出的 JSON 信封 created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL );

源码中的建表与索引(含(status, updated_at)清扫索引)见 packages/think/src/think.ts。

6.1 管线:查、占、跑、结

  1. 授权与已批准输入校验之后,计算key:提供了idempotencyKeyaction:${actionName}:${action.idempotencyKey(input)},否则存在 tool call id 时回退tool:${toolCallId}
  2. 查询。若行存在:
    • settledinput_hash匹配 → 返回存储的result_json不重执行),发出action:ledger:replayed
    • 同 key 但不同动作/输入哈希 → 结构化ActionKeyConflict(幂等键被不同输入复用——编程错误);
    • pending→ 早前尝试在途或执行中崩溃。若同 isolate 的活跃 Promise 拥有该 key 则等待它;否则应用pending-retry 租约:仅当动作显式提供了idempotencyKey且行已过期(updated_at早于actionLedgerPendingRetryLeaseMs,默认 5 分钟),才就地回收(刷新updated_at/request_id/tool_call_id,仍是pending——这是所有权租约而非新状态),发出action:ledger:reclaimed重跑execute(显式键是开发者对"这安全"的断言)。新行、回退tool:键、或租约被禁用(false)则抛出结构化ActionPendingError,不重执行。
  3. 插入pending,注册同 isolate 的活跃 Promise 用于合并,运行execute
  4. 成功:把已准备、JSON 往返过的模型可见输出存为settled。抛错/超时:删除 pending 行并返回常规结构化动作错误,允许后续重试再跑。
  5. 保留清扫尽力而为:settled行默认 30 天、pending行默认 90 天,可逐状态禁用。保留与重试租约是两回事:保留决定旧行何时被删除;租约决定过期pending行何时可被回收。让pending保留时长明显大于租约,保证回收先于清扫发生。

租约/保留配置常量见 packages/think/src/think.ts;认领逻辑_claimActionLedgerRow(读后立即决策,无await间隙,单 DO isolate 内两 claim 不会交错)见 #L9367 起。

6.2 与既有 keyspace 协调("单一 keyspace")

  • 账本是独立表,但key可从上游身份推导,使 webhook → submission → action 链路端到端去重:当动作是某个已去重事件的副作用时,调用方应让idempotencyKey纳入cf_think_submissions.idempotency_keyidempotencyKeyForEvent值。RFC 不自动耦合(那会令人意外),而是记录模式、让动作从输入/上下文推导稳定语义键。
  • Transcript 的 first-write-wins 仍然适用,它是模型可见的去重;账本是副作用去重。二者互补:transcript 防止模型重复调用;账本防止恢复重试重复执行。
  • 与恢复 RFC 协调:账本在每次动作执行时被咨询,包括恢复重入(Turns RFC 的recovery-continue/recovery-retry)。恢复引擎"已结算的工具结果不会被意外重放"的承诺在此为副作用实现——当动作键在重入路径上稳定时。

7. 恢复分类法:kind 决定崩溃行为

ActionKind映射到具体的恢复行为,与恢复适配器的classifyRecoveredTurn结果对齐,一套模型覆盖工具与渠道:

Kind中途崩溃时重放安全
server经恢复重试重跑账本对稳定键返回已结算结果;否则重执行
client客户端重新解析客户端解析;不由服务端执行
approval-gated停靠(等待交互 → 无预算消耗)仅在批准后重执行;账本在批准后适用
durable-pause停靠在持久化存储;经approveExecution恢复(连接无关)暂停行的 claim-by-delete 是去重锚点;已批准运行流经动作账本
delegated-agent子运行重挂接而非重启子代理自身的账本/恢复

kind 未显式指定时的推断规则approval已设置 →approval-gated;无服务端executeclient;委派给子代理 →delegated-agent;否则serverdurable-pause永不推断——它改变回合生命周期(回合结束、稍后恢复),必须显式请求kind: "durable-pause"

8. 护栏:把只存在于个别工具的防护标准化

  • 每动作超时。按 kind 默认(server: 30s;client/approval/durable-pause: 无)。实现为链接到回合信号的AbortControllerctx.signal是组合信号。(此前只有 bash/execute 限时。)
  • 结构化工具错误。抛出的错误被捕获并映射为稳定信封,以现有output-errorpart 状态携带errorText呈现,流不会因工具抛错而崩溃,模型看到可推理的{ error: { name, message } }
  • 输出截断。大输出带截断通知,复用truncateToolOutput语义,溢出记为可观测性。
  • 安全序列化。输出持久化前防御性序列化(处理循环引用/bigint),复用normalizeToolInput风格的安全措施。

这些都是默认值,每个都可按动作覆盖。已落地的编译工具目前执行超时/中止、结构化错误映射、JSON 安全输出归一化(含 bigint 与循环引用处理)、输出截断、授权检查与 approval-gated 的needsApproval推导;outputSchema校验、账本查询与账本写入为后续切片。

9.ctx.attachReply:不改模型所见,只附加交付意图

类型化的交付元数据侧信道,影响最终回复的渲染方式而不改变工具的模型可见输出。此前 Messenger 交付只保留文本增量,工具无法说"请以音频交付"。

type ReplyAttachment = | { type: "voice_note" } | { type: "email_draft"; subject?: string; to?: string[] } | { type: "card"; payload: unknown } | { type: string; [k: string]: unknown }; // 开放联合;渠道可扩展 const markAsVoiceNote = action({ description: "Send the final reply as a voice note", inputSchema: z.object({}), async execute(_input, ctx) { ctx.attachReply({ type: "voice_note" }); return { acknowledged: true }; } });

实现:附件按requestId累积在活跃回合上,在现有响应钩子点(onChatResponse/_fireResponseHook)通过ChatResponseResult.attachments暴露;Think 还提供replyAttachments(requestId?)服务端 getter 供编程调用方在回合完成后检查。本 RFC 只定义记录 API 与开放的ReplyAttachment联合,不渲染任何内容——渲染归 Channels/Voice RFC。

已落地语义

  • 附件是建议性、尽力而为的:非法值被忽略,记录时 JSON 归一化(bigint/循环/function/symbol 不会破坏下游持久化或 RPC),读取时深拷贝快照,每回合上限丢弃多余。
  • 策略回调(approvalpermissions、函数型idempotencyKey)收到空操作attachReplyexecute附加后又抛错/中止的附件被丢弃。
  • 生命周期:普通 server 动作与批准后的 approval-gated 动作可在产生回合附加元数据;durable-pause 已批准动作在 v1 是空操作——审批在原始暂停行下运行execute,结果由稍后带新requestId的续跑回合交付。这与账本重放同规则:附件仅在产生尝试上保证,execute被跳过时不重新应用。

类型集成、可观测性、版本兼容

类型集成

  • action<Input, Output>inputSchema推断Inputexecute全程类型化、零代码生成。
  • getActions()返回Record<string, Action>,注册键即默认动作名。
  • 框架发现/manifest 已建模工具(ThinkFrameworkTool),发现层可额外记录动作的name/permissions/approval/kind,让think inspect与文档列出它们;生成的think.d.ts不需要按动作生成类型——TS 推断已覆盖execute

可观测性:action:*事件族

chat:recovery:*chat:turn:*平行(源码事件类型定义见 packages/think/src/think.ts 附近):

  • action:invoked{ requestId, toolCallId, action, kind }
  • action:authorized/action:denied{ action, required, granted }
  • action:approval-requested/action:approval-resolved{ approved }
  • action:settled{ durationMs, truncated }
  • action:replayed{ from: "ledger" }
  • action:timed-out/action:error{ name, message }
  • action:reply-attached{ action?, attachmentType }

版本与兼容

@cloudflare/think为 0.9.x(pre-1.0),这些是带 changeset 的增量 minor 变更。新增:action()导出、getActions()钩子(默认{})、authorizeTurn/authorizeAction钩子(默认全授权 → 无行为变化)、动作审批 part 上的审批描述符、ctx.attachReplyChatResponseResult.attachments。不变:getTools()beforeToolCall/afterToolCall、两套审批路径——动作下游是普通工具,与它们全部可组合。

测试策略与边界不变量

测试策略

  1. 转换单元测试actionToTool产出合法 AI SDK 工具;needsApprovalapproval推导;beforeToolCall仍门控。
  2. 授权测试:默认全授权向后兼容;必需 ⊄ 已授权 → 结构化output-error,不执行;自定义authorizeAction
  3. 账本测试:已结算键重放存储结果不重执行;复用键 + 不同输入报错;pending窗口策略;从 submission/event id 推导的键端到端去重。
  4. 审批流测试approval-gated驱动现有approval-requested → tool-approval → auto-continuation路径;durable-pause停靠并经approveExecution恢复;part 上有稳定描述符。
  5. 回复附件测试ctx.attachReply在普通与批准后的 approval-gated 执行上记录 JSON 安全附件、按回合限量、暴露在ChatResponseResult.attachments/replyAttachments()、忽略谓词/durable-pause-resume 空操作调用、账本重放不重发。
  6. 护栏测试:超时经ctx.signal中止;抛错 →output-error(流存活);截断 + 通知;循环/bigint 的安全序列化。
  7. 恢复协调测试:已结算动作在recovery-continue/recovery-retry上不重执行;复用 deploy-churn e2e 风格(已用tool_ledgerfixture)证明跨崩溃无双重副作用。

仓库中真实存在的相关测试可作参考:packages/think/src/tests/actions-durable-pause.test.ts(覆盖"未跑副作用即停靠""pendingApprovals 列表与描述符""批准恰好执行一次并清行""拒绝不执行并清行""二次 approve 报错且 claim-by-delete 绝不双执行""并发 approve 单胜出""谓词 false 内联运行""谓词 true 停靠""重复 approve 幂等键去重""TTL 清扫遗弃行但保留新行""连接无关的回合驱动续跑""孤儿 durable-pause 结果不重触发""action()拒绝 durable-pause + approval: false"等),以及 packages/think/src/e2e-tests/action-ledger-recovery.test.ts 与 action-pause-recovery.test.ts。

关键边界与不变量

  • beforeToolCall优先级:保持最外层闸门;它block会在授权/账本/execute 之前短路,行为不变。
  • 客户端动作永不触达账本或服务端 execute——编译为仅 schema 的客户端工具。
  • 账本按已结算结果生效而非按尝试——failed行不阻塞合法重试;只有settled短路。
  • 审批拒绝对该调用是终结性的——映射output-denied;动作不执行,账本不记录。
  • attachReply从属于成功的execute——审批谓词、权限策略、函数型幂等键收到空操作记录器;execute 附加后失败/中止则回滚。
  • 超时 vs 回合中止——ctx.signal任一即触发;动作必须把中止当"立即停止",部分副作用是动作自身责任(账本记failed/pending,不记部分成功)。
  • 全授权默认——应用覆盖authorizeTurn之前所有动作都被允许,给动作加permissions直到授权接线前都是非破坏性的。
  • 先授权后审批——未授权的 approval-gated 动作立即返回output-error,永不进入审批提示。
  • 账本重放不重发附件——重放动作返回存储的result_json而不跑execute,其attachReply副作用不重触发。

备选方案与开放问题

被拒绝的备选:把一切写进beforeToolCall文档(保持面小但把权限/幂等/护栏留成命令式应用代码——正是要修的状态);只用 AI SDKneedsApproval(只覆盖审批,无稳定描述符/权限/幂等/护栏,但被复用为后端);幂等只按toolCallId(单回合内崩溃安全,但不能跨 webhook 重试/submission 去重,保留为回退键);默认从输入哈希自动推导幂等键(危险:两次合法的相同退款会合并,默认拒绝、可显式选用);只把动作放进getTools()自动检测(弄脏ToolSet类型,作为可选糖提出);独立@cloudflare/actions包(为时过早,动作与 Think 回合/恢复内部紧耦合)。

开放问题

  • pending账本窗口:动作已插入pending、执行了副作用、在标记settled前崩溃——恢复时无法知晓副作用是否发生。选项:要求副作用动作外部幂等并重执行(默认);暴露"未知结果"错误需人工/补偿处理(可选);两阶段prepare/commit动作形态。
  • 授权来源:Channels RFC v1 未带渠道授权,authorizeTurn尚不接收渠道注入的默认授权;ChannelContext已留有未来grants字段的接缝。
  • attachReply交付生命周期与重放:是否把附件持久化进账本使其在已结算重放后存活(v1 视为尽力而为、仅产生尝试)。
  • action()包位置:顶层 vs@cloudflare/think/actions子路径。
  • 账本保留/TTL 与大小上界(settled 行会累积)。
  • getActions()getTools()是否最终合并

实现地图:在仓库中继续深入

  • 主类与动作核心:packages/think/src/think.ts——action()/isAction()/ActionKind/ActionConfig/ActionContext(#L1340 起)、getActions()钩子(#L4451)、authorizeTurn/authorizeAction(#L5552 起)、账本与暂停表(#L9325 起、#L9628 起)、pendingApprovals/approveExecution(#L13998 起)。
  • 工具合并与beforeToolCall/afterToolCall_runInferenceLoop
  • 审批/HITL:_applyToolApprovalapproveExecution/rejectExecution_scheduleAutoContinuation;part 状态工具在packages/agents/src/chat/tool-state.tsoutput-error/output-denied/errorTextpackages/agents/src/chat/message-builder.ts
  • 客户端工具(clientkind 的编译目标):packages/agents/src/chat/client-tools.ts
  • 需协调的幂等 keyspace:cf_think_submissions.idempotency_keyidempotencyKeyForEventpackages/think/src/messengers/chat-sdk.ts)。
  • 护栏模式复用:truncateToolOutput与 bash 时限(packages/think/src/tools/workspace.ts)、execute 截断(packages/think/src/tools/execute.ts)、normalizeToolInputpackages/agents/src/chat/message-builder.ts)。
  • attachReply的投递钩子点:_fireResponseHook/onChatResponsepackages/think/src/messengers/delivery.tsTextStreamCallback
  • 相关 RFC:动作运行在回合内并共享恢复分类法(design/rfc-think-turns.md);账本与恢复引擎的 replay/progress 契约对齐(design/rfc-chat-recovery-foundation.md);Think 总览(design/think.md);delegated-agent动作对应的子代理编排(design/agent-tools.md)。

结论

Think Actions 把权限、审批、幂等、护栏与恢复分类从"应用民间传说"收编为声明式、可组合、增量兼容的框架能力:action()描述符编译成普通 AI SDK 工具,自动流经现有beforeToolCall/afterToolCall路径;账本让已结算副作用在恢复/重试时防重放;统一的审批描述符让 Web、语音、Messenger 与工作流用同一份数据渲染审批;ctx.attachReply为未来渠道交付预留接缝。无论你的 Agent 是需要退款审批、子代理委派、还是跨部署暂停恢复,这套设计都能让你少写一套"自己的框架",把注意力放回真正的业务逻辑。

【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 22:49:44

基于用户的协同过滤工程实现:从MySQL评分矩阵到Django推荐服务

简介&#xff1a;本资源是一份面向计算机专业本科生的毕业设计论文&#xff0c;聚焦基于Python与协同过滤算法的电影推荐系统实现&#xff0c;适用于毕业设计选题参考、课程设计实践及推荐系统入门学习。论文完整覆盖系统需求分析、Django框架开发、MySQL数据库设计、协同过滤算…

作者头像 李华
网站建设 2026/9/17 22:49:38

高效协作新范式:模块化自治与接口化开发实践

1. 反直觉的合作悖论第一次听到"人类最有效的合作方式就是不合作"这个说法时&#xff0c;我正参与一个跨国研发项目。当时团队陷入典型的"三个和尚没水喝"困境——每周要开7场协调会&#xff0c;40%时间花在进度同步上&#xff0c;核心功能开发反而停滞不前…

作者头像 李华
网站建设 2026/9/17 22:49:24

小白羊云盘gaozhangmin最新版

链接&#xff1a;https://pan.quark.cn/s/adafead25115基于阿里云盘开放平台API的新版小白羊阿里云盘客户端。登录: 阿里云Open API相比之前的版本功能受限&#xff0c;只开放了很少量的功能&#xff0c;如果完全弃用旧版API,小白羊的功能会大打折扣。 因此&#xff0c;项目基于…

作者头像 李华
网站建设 2026/9/17 22:48:35

ASP.NET Core高效开发框架aspnetx实战指南

1. 项目背景与核心价值第一次听到"哥本哈士奇(aspnetx)唤"这个项目名称时&#xff0c;很多.NET开发者都会会心一笑。这个看似戏谑的名字背后&#xff0c;其实是一个针对ASP.NET Core应用的高效开发框架。它就像哈士奇一样——外表活泼搞怪&#xff0c;但工作起来异常…

作者头像 李华
网站建设 2026/9/17 22:48:17

微信聊天记录导出快速指南:20分钟把五年对话变成可搜索的存档

微信聊天记录导出快速指南&#xff1a;20分钟把五年对话变成可搜索的存档 【免费下载链接】WeChatMsg 提取微信聊天记录&#xff0c;将其导出成HTML、Word、CSV文档永久保存&#xff0c;对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/w…

作者头像 李华