`Next` Hook 在 LLM 响应完成、所有工具调用执行结束之后运行。用它来处理结果、持久化数据、发送自定义消息,或将 Agent 推入下一轮执行。
## 最简实现
```typescript
// assistants/my-assistant/src/index.ts
import { agent, Process } from "@yao/runtime";
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
return null; // 默认:直接将 LLM 响应呈现给用户
}
```
## Payload
```typescript
interface Payload {
messages: agent.Message[]; // 发送给 LLM 的完整消息(含历史)
completion?: {
content: string | ContentPart[]; // LLM 文本响应,或多模态内容块
tool_calls?: ToolCall[]; // LLM 请求的工具调用
reasoning_content?: string; // 推理过程(o1 / DeepSeek R1 模型)
finish_reason?: string; // "stop" | "length" | "tool_calls" | "content_filter"
usage?: UsageInfo; // Token 用量
};
tools?: ToolCallResponse[]; // 工具调用执行结果
error?: string; // LLM 或工具执行失败时设置
}
```
`tools` 中每个 `ToolCallResponse`:
```typescript
interface ToolCallResponse {
toolcall_id: string; // 本次调用的唯一 ID
server: string; // MCP Server ID
tool: string; // 工具名称
arguments?: any; // 传给工具的参数
result?: any; // 工具返回值
error?: string; // 工具调用失败时设置
}
```
## 返回值
返回类型是 `agent.Next | null`。
**`null`** —— 默认。将 LLM 的 `completion.content` 原样呈现给客户端。
**`{ data }`** —— 用自定义数据替换标准响应。`data` 值作为最终响应的 `Next` 字段返回给调用方。
```typescript
return { data: { status: "success", id: 42 } };
```
**`{ delegate }`** —— 跳过当前响应,将请求交给另一个 Agent。被委托的 Agent 与当前 Agent 共享 `ctx`(相同 space、writer、stack trace)。详见[下方委托章节](#从-next-委托)。
```typescript
return {
delegate: {
agent_id: "yao.specialist", // 必填
messages: [...], // 必填 —— 发送的消息
options: { metadata: {} }, // 可选 —— 覆盖 connector、locale 等
},
};
```
**`{ data, metadata }`** —— `metadata` 仅用于调试/追踪,不影响输出。
```typescript
return {
data: { status: "done" },
metadata: { duration_ms: 120, steps: 3 },
};
```
> **决策逻辑**(来自 `next.go` 中的 `processNextResponse`):
> 1. `null` → 标准响应(呈现 LLM completion)
> 2. `{ delegate }` → 调用目标 Agent,返回其响应
> 3. `{ data }` → 将 `data` 包装进响应,跳过 completion 呈现
> 4. `{}`(空,无 delegate 或 data)→ 等同于 `null`
## 常见模式
### 处理错误
> **注意:** 在当前流水线中,如果 LLM 或工具执行失败,Agent 会在 Next Hook 运行之前提前返回,因此 `payload.error` 很少被设置。此处的防御性判断仍然有用,可作为未来变更或自定义调用路径的安全网。
```typescript
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
if (payload.error) {
ctx.Send({
type: "error",
props: { message: payload.error, code: "llm_error" },
});
return { data: { status: "error" } };
}
return null;
}
```
### 解析结构化输出
当 LLM 返回 JSON 时,在 Next Hook 中解析:
```typescript
import { agent, Process } from "@yao/runtime";
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
const raw = payload.completion?.content;
const content = typeof raw === "string" ? raw : "";
let result: any;
try {
const match = content.match(/```json\n([\s\S]*?)\n```/);
result = JSON.parse(match ? match[1] : content);
} catch {
ctx.Send("解析响应失败,请重试。");
return { data: { status: "parse_error" } };
}
// 通过 Yao Process 保存到数据库
const id = Process("models.bookmark.Save", result);
ctx.Send(`已保存:${result.title}`);
return { data: { status: "success", id } };
}
```
### 发送导航动作
保存数据后,将用户导航到结果页面:
```typescript
ctx.Send({
type: "action",
props: {
name: "navigate",
payload: { route: `/agents/yao.my-assistant/detail?id=${id}` },
},
});
```
### 读取 Create Hook 中存储的状态
Create Hook 存入 `ctx.memory.context` 的数据在这里可以读取:
```typescript
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
const startTime = ctx.memory.context.Get("start_time") as number;
const duration = Date.now() - startTime;
const mode = ctx.memory.context.Get("mode") as string;
ctx.Send(`完成,耗时 ${duration}ms(模式:${mode})`);
return null;
}
```
## 从 Next 委托
根据 LLM 的响应路由到另一个 Agent:
```typescript
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
const raw = payload.completion?.content;
const content = typeof raw === "string" ? raw : "";
if (content.includes("[[ESCALATE]]")) {
return {
delegate: {
agent_id: "yao.senior-agent",
messages: payload.messages,
},
};
}
return null;
}
```
## 多步循环
**这是驱动自主多步执行的核心机制。** `Next` Hook 返回指向同一 Agent 的 `delegate`,触发新一轮 Create → LLM → Next 循环。
> **注意:** 每次 `delegate` 调用都是一次全新的 Agent 执行。使用 `ctx.memory.chat`(而非 `ctx.memory.context`)在各轮之间持久化步骤计数器 —— `memory.context` 与当前请求上下文绑定,上下文释放时清除(默认 TTL:30 分钟)。
```typescript
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
// 使用 chat 级别记忆 —— 跨 delegate 轮次存活
const step = (ctx.memory.chat.Get("step") as number) || 0;
if (step < 3 && needsMoreWork(payload)) {
ctx.memory.chat.Set("step", step + 1);
// 追加 LLM 响应和后续指令
const next_messages = [
...payload.messages,
{ role: "assistant", content: payload.completion?.content },
{ role: "user", content: "Continue with the next step." },
];
return {
delegate: {
agent_id: ctx.assistant_id, // 同一个 Agent —— 触发下一轮
messages: next_messages,
},
};
}
ctx.memory.chat.Del("step"); // 完成后清理
return null;
}
```
## 真实示例:Mark 的 Next Hook
来自 `yaobots/assistants/yao/mark/src/index.ts` —— 读取 Sandbox 输出,保存到 DB,跳转导航:
```typescript
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next {
if (payload.error) {
return { data: { status: "error", message: payload.error } };
}
const canvasFiles = readCanvasFiles(ctx);
const chatId = ctx.memory.context.Get("chat_id") || ctx.chat_id;
const mode = ctx.memory.context.Get("mode") || "create";
const fileId = saveJSAttachment(canvasFiles.js, `canvas_${Date.now()}.js`);
const { metadata } = canvasFiles;
const auth = getAuth(ctx);
let canvasId: string;
if (mode === "edit") {
canvasId = ctx.memory.context.Get("canvas_id");
updateCanvasRecord(ctx.memory.context.Get("record_id"), fileId, metadata, auth);
} else {
canvasId = generateCanvasId();
createCanvasRecord(canvasId, fileId, chatId, metadata, auth);
}
// 导航到结果页面
ctx.Send({
type: "action",
props: { name: "navigate", payload: { route: `/agents/yao.mark/index?canvas=${canvasId}` } },
});
const duration = Date.now() - (ctx.memory.context.Get("start_time") as number);
ctx.Send(`书签"${metadata.title}"已生成!(${duration}ms)`);
return { data: { status: "success", canvas_id: canvasId } };
}
```
## 下一步
你已经看到 Hook 能做什么。接下来了解 Hook 内部可用的全部 API。
→ **[Context API](./context-api)**