操作
操作是智能体行为的一个单元。它查看当前会话和对话,判断自己是否有工作要做,然后返回效果,或把控制权交还给调用方。状态机决定何时询问它;运行时决定如何存储它的效果。
这比工具的范围更广。工具是模型可以请求的能力,例如读取文件。操作则决定智能体如何继续:执行待处理的工具调用、要求审批、添加项目上下文、压缩历史,或在达到预算后停止。有些操作还会向推理提供工具或提示词内容。
操作契约
针对你的会话类型 S 和效果类型 E 实现 Operation<S, E>。它的主入口 run 接收已加载的会话、对话和 emitter。返回结果控制状态机接下来做什么:
| 结果辅助函数 | 含义 |
|---|---|
not_applicable() | 这个操作无事可做,尝试下一个步骤。 |
applied(effects) | 应用这些效果,重新加载会话,并从第一个步骤重新开始。 |
yielded() | 不添加效果,直接把控制权交还给调用方。 |
yielded_with(effects) | 保存这些效果,然后把控制权交还给调用方。 |
操作可以实现 inference_tools 来公布工具,实现 prompt_parts 来贡献系统提示词的各个部分,实现 moim_parts 来提供请求准备器可以放在当前回合附近的上下文。这些贡献在推理之前收集,与最后是哪个操作应用了效果无关。如何使用它们由请求准备器决定。
Inference 同样实现 Operation,但它放在 Step::Inference 中,这样状态机会调用它的推理方法并收集这些贡献。普通操作根本不必调用模型。
完整示例:回合预算
假设应用对每个用户请求最多允许三个助手回合。保存了当前的工具结果之后,它应该把控制权交还给用户,而不是再发起一次模型调用。
我们可以在不改动推理或状态机的情况下实现这一点。下面这个原生 Rust 操作适用于任何会话,以及任何能表示 Message 的效果类型:
use anyhow::Result;
use async_trait::async_trait;
use goose_agent::operation::{
assistant_turn_count, ends_turn, messages_since_kickoff,
not_applicable, yielded_with, Emitter, Operation, OperationResult,
};
use goose_provider_types::conversation::{message::Message, Conversation};
struct TurnLimit {
max_turns: u32,
}
#[async_trait]
impl<S, E> Operation<S, E> for TurnLimit
where
S: Send + Sync,
E: From<Message> + Send + 'static,
{
fn name(&self) -> &'static str {
"turn_limit"
}
async fn run(
&self,
_session: &S,
conversation: &Conversation,
emit: &Emitter,
) -> Result<OperationResult<E>> {
let messages = messages_since_kickoff(conversation)?;
if ends_turn(messages) || assistant_turn_count(messages) < self.max_turns {
return not_applicable();
}
let message = Message::assistant()
.with_text("I've reached this request's turn limit. Would you like me to continue?");
yielded_with([emit.message(message).into()])
}
}
messages_since_kickoff 选出当前的用户请求以及它之后的消息。assistant_turn_count 统计的是助手消息块的数量,而不是单个流式分片。当智能体已经自然结束,或仍在预算之内时,这个操作会拒绝。否则,它会发出一条可见的消息,并把同一条消息作为效果返回,在让出之前保存。
在你的组装代码中,把它加在工具操作之后、推理之前:
let machine = StateMachine::new(
vec![
Step::Operation(Arc::new(tools)),
Step::Operation(Arc::new(TurnLimit { max_turns: 3 })),
Step::Inference(Arc::new(inference)),
],
cancel,
);
这里的 tools、inference 和运行时来自你的智能体组装,和 Hello World 中一样。这个顺序是有意的:第三个助手回合中的工具请求仍然会得到响应。响应保存之后,预算操作会在第四次推理调用开始之前让出。如果把限制放在工具之前,会更早停止,但可能留下没有响应的工具调用,需要另行处理。
用户发送新消息时,它会成为下一个起点,计数重新开始。没有任何可变计数器需要在重启后保留。这是对助手回合数的限制,不是金额、token 或工具调用次数的预算。完整的 goose 应用有自己的 MaxTurnsOperation,带有额外的提示词上下文。
目前可用的操作
可复用的 GDK 组件
这些是 goose-agent 中的公共 API;所需的工具、提供商或子智能体运行器由你的应用提供:
| 组件 | 用途 |
|---|---|
ToolOperation | 公布并执行带类型的同步或异步工具,或由 ToolProvider 动态提供的工具。 |
SubagentOperation | 通过你的 SubagentRunner 运行被请求的子智能体会话,传递它们的更新,并处理并发和取消。 |
InferenceRunner | 准备提供商请求,流式推理,并返回消息和用量效果。把它注册为推理步骤。 |
goose 中的操作
goose 用同样的接口组合出一个更大的智能体。下面这些是 goose 应用中的实现,不是稳定、可复用的 GDK API。它们依赖 goose 的会话、配置、扩展和钩子。请把它们当作操作可以负责哪些行为的示例,而不是供你的应用导入的清单。
| 操作 | 用途 |
|---|---|
ToolApprovalOperation | 检查待处理的工具请求,并记录它们可以直接执行还是需要审批。 |
ToolExecutionOperation | 执行符合条件的扩展工具调用,包括工具钩子和流式输出。 |
RemainingToolCallsOperation | 响应被拒绝、无效或未处理的调用,并在审批待定时让出。 |
SkillOperation | 发现并加载 skill,贡献它们的上下文,并处理 skill 命令。 |
RecipeOperation | 应用 recipe 的指令和工具,处理 recipe 命令,并收集结构化的最终输出。 |
SteerOperation | 在模型回合和工具回合之间纳入排队的用户引导。 |
BangShellOperation | 把以 ! 开头的用户消息转成直接的 shell 工具请求。 |
CompactionOperation | 自动或通过命令压缩对话历史。 |
ToolPairCompactionOperation | 摘要较旧的工具请求/响应对,以缩小上下文。 |
DoctorOperation | 通过 /doctor 运行会话诊断并提供修复上下文。 |
ProjectOperation | 把所选项目的指令贡献给模型请求。 |
RetryOperation | 评估 recipe 的成功检查,以及 goal/grind 的继续或重试行为。 |
EmptyResponseOperation | 当没有其他操作接管时,让空响应的兜底处理变得可见。 |
StopHookOperation | 让已配置的停止钩子接受或阻止回合的结束。 |
ExitOnErrorOperation | 当对话结尾仍有错误时让出。 |
MaxTurnsOperation | 强制执行 goose 的自主回合预算,并贡献剩余预算的上下文。 |
AutoEffortOperation | 启用自动思考强度选择时,为某个回合选择并记录思考强度。 |
SlashCommandOperation | 把识别到的斜杠命令路由到各操作的命令处理器。 |
StatusOperation | 通过 /status 报告当前的会话、模型和用量。 |
状态机构建器展示了主要的组合方式和顺序。操作模块包含这些实现;有些操作只在相关配置下才会启用,或通过命令处理器调用。
如何以及为何编写自己的操作
当应用需要一条规则或生命周期步骤,而它不应该依赖模型选对工具时,就编写一个操作。例如强制执行支出上限、等待外部事件、验证结果,或添加特定领域的上下文。如果你只是需要一个模型可以调用的新能力,就改为向 ToolOperation 添加工具。
先确定你的操作适用的条件。让它的效果改变这个条件,或者让出,这样它就不会在没有进展的情况下一直选中自己。再选择它相对于其他步骤的位置:审批关卡必须在工具执行之前,而我们的预算示例则有意放在其后。
把需要长期保留的决策保存在会话、对话元数据或运行时加载的其他存储中。set_message_meta 和 message_meta 可以帮助标记操作已经做过什么。如果某个步骤在 await 期间持有已收到的输出,请实现 finalize_cancellation,返回尚未保存的输出。取消不会回滚外部动作。
更大的示例见 GooseBusters,它用自定义操作来协调一次电话通话。完整的契约和结果类型见 goose-agent::operation。