本文同步自仓库
docs/core-integration.md。文中指向源码的链接跳到 GitHub 仓库, 站内可读的相关页面已改为站内链接。
把 @zqagent/core 接入第三方系统:完整接入手册
简体中文 · 对应当前仓库 0.3.25
本文面向要把 AI Agent 能力嵌进自有系统的团队:你的系统可能是一个 Web 服务、 桌面应用、任务队列 Worker,或者已经有一套会话/用户/计费体系的产品。 目标是不安装 CLI、不启动 zqagent 的 server,只把 @zqagent/core 当作库, 拿到「模型 + 工具 + 权限 + 事件流 + 持久化」这一整套 Agent 能力。
只想先跑一个最小例子:看 embedding.md 和
examples/embed-basic。 本文是它的完整版,覆盖全部公开 API、集成模式与生产注意事项。
- 引擎实现:
packages/core/src/engine/TurnEngine.ts - 包入口与全部导出:
packages/core/src/index.ts - 包配置(
exports/ 依赖 / 构建):packages/core/package.json - 仓库架构总览:architecture.md
- 内部服务端参考实现:
packages/server/src/session.ts
目录
- 它是什么,不是什么
- 架构定位与包关系
- 安装与运行环境
- 五分钟跑通
- 核心概念
- 装配引擎
- 工具:内置与自定义
- 权限、审批与安全边界
- 事件流完整参考
- 模型 Provider
- 生命周期钩子与持久化
- 一轮 Turn 的控制
- 附件
- 上下文压缩
- MCP 接入
- 子代理与 Agent 注册表
- 变更追踪与回滚
- 四种集成模式与代码骨架
- 生产检查清单
- API 速查
- 排错 FAQ
- 版本、构建与发布
1. 它是什么,不是什么
@zqagent/core 是一个 Agent 引擎库。它负责:
- 维护一次会话的消息历史;
- 调用 LLM(流式),解析文本、推理过程与工具调用;
- 通过
ToolRegistry执行工具; - 通过
PermissionEngine判断每个工具调用该放行、拒绝还是问人; - 以异步生成器事件流的形式把全过程暴露给调用方;
- 在上下文超限时压缩历史;
- 可选:持久化事件与检查点、MCP 接入、子代理、Git 快照。
它不负责:
- UI / 终端渲染(
ConsoleHook只是参考实现); - HTTP / WebSocket / SSE 服务(
@zqagent/server是参考实现); - 用户、租户、计费、鉴权;
- 会话索引与数据库(
FileLogHook是文件级参考实现); - 替调用方做审批决策。
一句话:你负责产品外壳与会话生命周期,引擎负责一轮对话怎么跑。
2. 架构定位与包关系
┌─────────────────────────────┐
你的系统 ──────▶│ @zqagent/core │
(Web/桌面/Worker)│ TurnEngine / ToolRegistry │
│ PermissionEngine / Provider │
│ 内置工具 / MCP / 压缩 / VCS │
└──────────────┬──────────────┘
│ 依赖
▼
┌─────────────────────────────┐
│ @zqagent/shared │
│ 模型能力表 / 协议 / 加密等 │
└─────────────────────────────┘
仓库自带的两个「完整应用」,可作为参考实现:
@zqagent/server = 多租户会话 + HTTP/SSE + 挂起式审批
@zqagent/cli = 终端渲染 + 会话持久化 + 交互审批依赖方向是单向的:shared ← core。core 不知道 server / cli 的存在, 也不会反向依赖它们。第三方系统引入 core 时不会把 HTTP 服务、Tauri、 控制台 UI 等一起拖进来。
仓库中的包:
| 包 | 用途 | 是否可独立使用 |
|---|---|---|
@zqagent/core | Agent 引擎(本文主角) | 是 |
@zqagent/shared | core 与 hub 共用的基础模块 | 作为 core 的依赖自动安装 |
@zqagent/server | 多租户 HTTP 服务参考实现 | 是,但属于应用层 |
@zqagent/cli | 终端应用参考实现 | 是,但属于应用层 |
3. 安装与运行环境
3.1 环境要求
| 项 | 要求 |
|---|---|
| Node.js | >= 18.17(package.json 的 engines) |
| 模块系统 | ESM 与 CommonJS 双产物,import / require 都能用 |
| 类型 | 类型声明随包发布(dist/index.d.ts / .d.cts) |
| 操作系统 | Linux / macOS / Windows;shell 工具在 Windows 会自动选择可用 shell |
3.2 安装
npm i @zqagent/core
# 或
pnpm add @zqagent/core
# 或
yarn add @zqagent/core版本核对:本文对应仓库
0.3.25。npm 上的可用版本以npm view @zqagent/core version为准;如果 npm 版本落后于仓库, 可在仓库内pnpm --filter @zqagent/core build后通过file:/ workspace 引用。 建议在package.json中锁定精确版本,避免引擎行为随小版本漂移。
3.3 TypeScript 配置
包使用 node16 模块解析,导出带 types 条件。推荐消费方:
{
"compilerOptions": {
"target": "es2022",
"module": "node16",
"moduleResolution": "node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}如果项目是纯 ESM("type": "module"),直接 import;如果是 CommonJS, require("@zqagent/core") 也可以,但需要 0.2.60 以上版本(更早版本的 CJS 产物会因 ESM-only 依赖而加载失败)。
3.4 子路径导入
packages/core/package.json 的 exports 是封闭的,只有下列子路径可用。 按需引入工具实现时走子路径,不要引用 dist/ 内部文件:
import { registerFileTools } from "@zqagent/core/tools/fs";
import { registerShellTool } from "@zqagent/core/tools/shell";
import { registerSearchTool } from "@zqagent/core/tools/search";
import { registerWebTools } from "@zqagent/core/tools/web";
import { registerGitTools } from "@zqagent/core/tools/git";
import { registerDiffTools } from "@zqagent/core/tools/diff";
import { registerTodoTool } from "@zqagent/core/tools/todo";
import { registerAskTool } from "@zqagent/core/tools/ask";
import { registerPlanTool } from "@zqagent/core/tools/plan";
import { registerDirectoryTool } from "@zqagent/core/tools/directories";
import { registerExploreTool } from "@zqagent/core/tools/explore";
import { registerMediaTool } from "@zqagent/core/tools/media";internal/* 子路径(internal/engine/FileLogHook、internal/mcp/config 等) 是给同仓库的 server / CLI 用的非稳定 API,第三方不应依赖;本文只使用主入口 与上面这些稳定子路径。FileLogHook、SavedState、MCP 配置函数等其实也在主入口 导出,优先用主入口。
4. 五分钟跑通
下面是一段完整可运行的 ESM 脚本(Node 18+):
// agent-demo.mjs
import {
ApprovalOutcome,
EventType,
Mode,
createDefaultEngine,
setQuestionHandler,
} from "@zqagent/core";
const engine = createDefaultEngine({
workspace: process.cwd(), // 权限边界:所有文件/命令都限制在这里
model: process.env.OPENAI_MODEL!, // 必填,不会兜底任何型号
mode: Mode.INTERACTIVE, // 有副作用的操作逐次审批
});
// 1) 审批回调:不注入时默认在第一次需要授权时抛错
engine.setApprover(async (request) => {
console.log(`\n需要审批: ${request.toolName} — ${request.reason}`);
console.log(request.arguments);
return ApprovalOutcome.ONCE; // 生产环境这里接你的 UI / IM / 工单
});
// 2) ask_user 的回答回调(模块级全局,注入一次即可)
setQuestionHandler(async (question, options, allowText, multi) => {
// 生产环境这里接你的 UI;示例直接返回第一个选项
console.log(`模型提问: ${question}`, options);
return options[0] ?? "";
});
// 3) 跑一轮,消费事件流
for await (const event of engine.run("读一下这个项目,告诉我它是做什么的")) {
switch (event.type) {
case EventType.ASSISTANT_DELTA:
process.stdout.write(String(event.data.text ?? ""));
break;
case EventType.TOOL_STARTED:
console.log(`\n[工具] ${event.data.name}`);
break;
case EventType.TOOL_FINISHED:
console.log(`[工具结束] ${event.data.name}: ${event.data.status}`);
break;
case EventType.ERROR:
console.error(`\n[错误] ${event.data.error}`);
break;
case EventType.TURN_END:
console.log(`\n[结束] ${event.data.status}`);
break;
}
}运行前设置:
export OPENAI_API_KEY=sk-...
export OPENAI_MODEL=gpt-4o-mini # 或任意 OpenAI 兼容模型
node agent-demo.mjs这一个脚本已经覆盖了嵌入时最重要的四件事:装配、审批、事件消费、多轮上下文 (同一个 engine 再调用一次 run() 就会带上上一轮历史)。
5. 核心概念
理解下面六个对象,后面的 API 就都能对上号。
| 概念 | 类型 | 职责 |
|---|---|---|
| Engine | TurnEngine | 一次会话的运行时。持有消息历史、provider、工具表、权限引擎 |
| Turn | engine.run() 的一次调用 | 用户一条输入 → 若干次 LLM/工具迭代 → 一个 turn_end |
| Iteration | 内部循环 | 一次 LLM 调用 + 该回复请求的全部工具执行 |
| Event | WsEvent | 引擎对外输出的唯一实时通道(异步生成器) |
| Tool | ToolSpec | 模型可调用的函数 + JSON Schema + 风险元数据 |
| Approver | Approver | 当权限引擎判定「需要问人」时,由调用方决定放行/拒绝 |
5.1 会话与 Engine 是一对一的
每个会话创建一个 TurnEngine,不要多个会话共享同一个 engine:
engine.messages是会话历史,天然串行;engine.permissions里的「本会话总是允许」是会话级状态;engine.compactionState是会话级压缩状态;- 同一个 engine 同时跑两轮
run()会互相污染历史。
多会话的正确做法是 Map<sessionId, TurnEngine>,每个会话一个实例,见 第 18 章。
5.2 事件流是主通道,Hook 是旁路
engine.run()的for await是主通道:有序、可背压、每个事件只出现一次, 适合直接转成 SSE / WebSocket 帧。EngineHook是旁路回调:同步、无返回值、异常会被吞掉,适合同一份数据的 第二消费方(落盘、计费、日志)。不要只靠 hook 驱动 UI,因为它不保证背压。
6. 装配引擎
有两条路径:
- 路径 A:
createDefaultEngine(opts)—— 一行装配全部内置能力,适合 90% 的场景; - 路径 B:
new TurnEngine(opts)—— 自己决定 provider、工具表、权限引擎、 系统提示词,适合要深度定制或复用已有基础设施的场景。
6.1 路径 A:createDefaultEngine
import { createDefaultEngine, Mode } from "@zqagent/core";
const engine = createDefaultEngine({
workspace: "/srv/workspaces/tenant-42",
model: "deepseek-chat",
mode: Mode.INTERACTIVE,
});它依次做这些事(见 packages/core/src/index.ts:409):
- 没有传
provider时创建new OpenAIProvider({ logDir }); - 解析沙箱方案
resolveSandbox(); - 创建
ToolRegistry与ConflictGuard,注册 21 个内置工具(见第 7 章); - 创建
PermissionEngine(工作区、模式、命令白名单、shell 策略、沙箱状态); - 没有传
instructions时用buildInstructions(agentType, workspace, today, lang)生成系统提示词; - 组装
TurnEngine;如果开了vcs,用CompositeHook把VcsHook并联进去; - 按
defaultApproval注入兜底 approver。
CreateEngineOpts 全参数
| 参数 | 类型 / 默认值 | 说明 |
|---|---|---|
workspace | string(必填) | 工作区根目录 = 文件与命令的权限边界 |
model | string,缺省读 OPENAI_MODEL | 模型标识;都为空时 run() 明确报错,不兜底型号 |
provider | ProviderClient | 自定义 provider;缺省创建 OpenAIProvider |
mode | Mode.INTERACTIVE | DISCUSS / PLAN / INTERACTIVE / AUTO |
maxIterations | DEFAULT_MAX_ITERATIONS = 800 | 单轮最大迭代次数 |
reasoningEffort | 缺省读 OPENAI_REASONING_EFFORT | low / medium / high;DeepSeek 自动跳过 |
allowedCommands | [] | Auto 模式下自动放行的命令前缀 |
shellPolicy | "approve" | approve / sandbox / workspace / all |
sandbox | 缺省 auto | { mode, extraWritable, ... },见 8.4 |
vcs | { mode: "off", sessionId: "default" } | 每轮 Git 快照,见第 17 章 |
instructions | 由 buildInstructions 生成 | 覆盖系统提示词 |
agentType | "code" | code(编程助手)/ cowork(通用协作),影响默认提示词 |
lang | 简体中文 | zh / en / zh-TW / ja / ko / es,影响提示词、命名、摘要 |
messages | [] | 初始对话历史(恢复会话时传入) |
hook | 无 | 生命周期钩子,见第 11 章 |
subagentHookFactory | 无 | 每次 explore 调用生成一个子代理 hook |
contextWindow | 自动检测,回退 128K | 模型上下文窗口(token) |
thresholdPct | 0.8 | 硬压缩阈值(占窗口比例) |
softThresholdPct | 硬阈值的 75% | 软阈值:只淘汰旧工具结果,不调 LLM |
capTokens | null | 硬阈值的绝对上限(token),null = 不限制 |
toolResultDir | 状态目录下 tool-results | 超预算工具结果的落盘目录;多租户必须按租户隔离 |
logDir | 不记录 | LLM 请求审计日志根目录(按日期分目录) |
defaultApproval | "error" | 未注入 approver 时:error / deny / allow |
lang 只影响默认提示词、会话命名与摘要的语言;显式传了 instructions 时以它为准。
6.2 路径 B:手动装配 TurnEngine
import {
ApprovalOutcome,
Mode,
OpenAIProvider,
PermissionEngine,
ToolRegistry,
TurnEngine,
registerFileTools,
registerShellTool,
registerSearchTool,
} from "@zqagent/core";
const workspace = process.cwd();
// 1) 工具表:只注册你需要的
const registry = new ToolRegistry();
registerFileTools(registry, workspace);
registerShellTool(registry, workspace);
registerSearchTool(registry, workspace);
// 2) 权限:工作区 + 模式 + 白名单
const permissions = new PermissionEngine({
workspaceRoot: workspace,
mode: Mode.INTERACTIVE,
allowedCommands: ["git status", "ls", "cat"],
});
// 3) Provider:任意 OpenAI 兼容端点
const provider = new OpenAIProvider({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL,
});
// 4) 引擎
const engine = new TurnEngine({
provider,
registry,
permissions,
model: process.env.OPENAI_MODEL!,
instructions: "你是一个只处理数据分析任务的助手。",
lang: "zh",
maxIterations: 50,
});
engine.setApprover(async () => ApprovalOutcome.DENY); // 别忘了TurnEngineOpts 全参数
| 参数 | 必填 | 说明 |
|---|---|---|
provider | 是 | ProviderClient(stream() / chat()) |
registry | 是 | ToolRegistry |
permissions | 是 | PermissionEngine |
model | 是 | 模型标识;空字符串会在 run() 抛错 |
instructions | 是 | 系统提示词,注入为首条 system 消息 |
genSessionNamePrompt | 否 | 生成会话名的提示词;缺省按 lang 取 |
lang | 否 | 默认 "zh";影响命名与压缩摘要 |
maxIterations | 否 | 默认 800 |
finalizeOnMaxIterations | 否 | 撞上限后再跑一轮无工具收尾;默认 false,子代理用 true |
reasoningEffort | 否 | 透传给 provider |
messages | 否 | 初始历史;若首条不是 system,会自动插入 instructions |
hook | 否 | EngineHook |
contextWindow | 否 | 覆盖自动检测 |
thresholdPct | 否 | 默认 0.8 |
softThresholdPct | 否 | 默认硬阈值的 75% |
capTokens | 否 | 默认 null |
toolResultDir | 否 | 工具结果落盘目录 |
6.3 运行时可以改什么
TurnEngine 的公开属性与方法:
engine.model = "another-model"; // 模型(下一轮生效)
engine.messages; // 完整历史(含 notice 消息)
engine.permissions.mode = Mode.AUTO; // 运行模式
engine.registry; // 工具表(可动态 enable/disable)
engine.compactionState; // 压缩状态(只读观察)
engine.approver; // 当前审批回调
engine.setApprover(fn); // 设置审批
engine.setSystemPrompt(text); // 运行时替换系统提示词(切 agent 用)
engine.requestInterrupt(); // 中断当前一轮
engine.queueSteering(text, attachments, refs); // 排队补充指令(后两个可选)
engine.removeSteering(text); // 撤销尚未注入的补充指令7. 工具:内置与自定义
7.1 createDefaultEngine 注册的 21 个工具
| 类别 | 工具 | 风险 | 注册函数 |
|---|---|---|---|
| 文件 | read_file, list_files | 只读 | registerFileTools / registerReadOnlyFileTools |
| 文件 | write_file, replace_in_file | 写本地 | registerFileTools |
| Shell | run_shell, shell_task_output, shell_task_kill | 执行 | registerShellTool |
| 搜索 | grep | 只读 | registerSearchTool |
| 网络 | web_search, web_fetch | 外部 | registerWebTools |
| Git | git_log, git_status, git_diff | 只读 | registerGitTools |
| 补丁 | apply_patch, apply_unified_diff | 写本地 | registerDiffTools |
| 协作 | ask_user, propose_plan, request_directory | 交互 | registerAskTool / registerPlanTool / registerDirectoryTool |
| 任务 | todo_write | 只读/状态 | registerTodoTool |
| 子代理 | explore | 只读 | registerExploreTool |
| 媒体 | media | 外部 | registerMediaTool |
终端工具不在一键装配里:
terminal_list/terminal_read依赖宿主提供的TerminalSource(控制台「终端」面板的长驻 PTY),需要自己注入:tsimport { registerTerminalTools, type TerminalSource } from "@zqagent/core"; registerTerminalTools(registry, { terminals: myTerminalManager, // 实现 TerminalSource projectId: "tenant-42", });
projectId是隔离边界,必须按项目传,否则 A 项目能读到 B 项目终端内容。
各工具的完整参数与行为见 内置工具。
7.2 自定义工具
ToolRegistry.register 的签名(packages/core/src/tools/ToolRegistry.ts:77):
type Register = (
func: (args: Record<string, unknown>, ctx?: { signal?: AbortSignal }) => unknown,
metadata: {
category: string;
riskLevel: "low" | "medium" | "high";
capabilities: string[];
requiresApproval: boolean;
},
schema?: ToolSchema, // 强烈建议显式给出
source?: string, // 展示用:"internal" / "mcp:xxx" / "myapp"
) => ToolSpec;示例:
registry.register(
async (args) => {
const orderId = String(args.order_id);
return await myOrderService.query(orderId);
},
{
category: "business",
riskLevel: "high",
capabilities: ["query_order"],
requiresApproval: true,
},
{
type: "function",
function: {
name: "query_order",
description: "按订单号查询内部订单系统,返回订单状态与金额",
parameters: {
type: "object",
properties: {
order_id: { type: "string", description: "订单号" },
},
required: ["order_id"],
},
},
},
"myapp",
);注意事项:
- 工具名来自
schema.function.name;不传 schema 时回退到函数名,参数结构为空, 模型基本不会正确调用,因此正式工具都应显式给 schema。 func可以返回 Promise;引擎会await。- 第二个参数
ctx.signal是中断信号。长耗时工具应把它传给底层任务, 否则用户中断后工具仍会跑完。 - 同名
register会替换旧工具,可用于覆盖内置实现;remove()删除;disable()/enable()控制是否出现在schemas()中(模型不可见)。 - 返回给模型的结果会经过结果预算:超过阈值会落盘并给模型一段摘要/路径, 可用环境变量
TOOL_RESULT_MAX_CHARS、TOOL_RESULT_SAVE_THRESHOLD、TOOL_RESULT_DIR调整。 - 工具抛出的异常会被引擎捕获,转成
{ error, error_type }作为工具结果, 并让模型自行决定下一步;不会让整轮崩溃。
7.3 只读工具集
给子代理或只读场景用:
import { registerReadOnlyFileTools } from "@zqagent/core";
registerReadOnlyFileTools(registry, workspace); // read_file / list_files8. 权限、审批与安全边界
8.1 决策流程
PermissionEngine.evaluate() 按固定顺序判断 (packages/core/src/permissions/PermissionEngine.ts:173):
- 只读模式(
DISCUSS/PLAN)→ 有副作用的操作直接拒绝; - 写文件路径不在
workspaceRoot下 → 直接拒绝; - 只读风险(
RiskClass.READ)→ 直接允许; AUTO模式 → 有副作用的操作全部允许;- shell 免审策略(
shellPolicy+ 沙箱)→ 可能直接允许; - 命令白名单 / 会话命令白名单 → 直接允许;
- 会话工具白名单(用户选过「总是允许该工具」)→ 直接允许;
- 其余 →
needsUser: true,交给 approver。
返回结构:
interface Decision {
allowed: boolean;
reason: string; // 展示给用户 / 审批卡片
needsUser: boolean; // true = 需要审批
}8.2 运行模式
enum Mode {
DISCUSS = "discuss", // 只读,禁止写入与执行
PLAN = "plan", // 只读,禁止写入与执行
INTERACTIVE = "interactive", // 有副作用操作逐次审批(推荐默认)
AUTO = "auto", // 有副作用操作全部放行
}interactive 是默认值,也是面向真实用户最安全的模式。auto 只应在隔离环境、 可丢弃的工作区或完全可信的输入下使用。
8.3 审批回调
type Approver = (request: PermissionRequest) => Promise<ApprovalOutcome>;
interface PermissionRequest {
toolName: string;
arguments: Record<string, unknown>;
metadata: unknown; // ToolMetadata | null
reason: string; // 权限引擎给出的原因
toolCallId?: string; // 与事件里的 reqId 对应
}
enum ApprovalOutcome {
ONCE = "once", // 仅本次
ALWAYS_TOOL = "always_tool", // 本会话总是允许该工具
ALWAYS_COMMAND = "always_command", // 本会话总是允许该命令(仅 run_shell)
DENY = "deny", // 拒绝
}实现方式:
- CLI / 桌面:阻塞等待用户在界面点按钮;
- Web 服务:把请求放进 Promise,向客户端推送审批卡片,收到回调后 resolve ——这就是
@zqagent/server的「挂起式审批」; - 无人值守 Worker:按白名单自动返回
ONCE,其余DENY(或defaultApproval: "allow",但要先确认工作区是隔离的)。
必须实现 approver。 默认 defaultApproval: "error" 会在第一次需要授权时抛错, 这是刻意设计:避免「忘了接审批 → 工具静默不执行 → 用户以为模型坏了」。
8.4 shell 免审与 OS 沙箱
shellPolicy:
| 值 | 行为 |
|---|---|
approve(默认) | 除白名单外一律审批 |
sandbox | 仅当 OS 级沙箱真正生效时免审,否则审批 |
workspace | 沙箱生效 → 免审;未生效 → 用工作区路径判定兜底 |
all | shell 全部免审(高风险,等同只对 shell 生效的 auto) |
沙箱配置:
createDefaultEngine({
workspace,
sandbox: {
mode: "auto", // auto / off / required
extraWritable: ["/data/cache"], // 额外可写路径
},
});- macOS:
sandbox-exec,把file-write*收窄到工作区 + 临时目录; - Linux:
bwrap,整机只读挂载 + 工作区可写; - Windows:没有等价的进程级文件系统沙箱,
enforced: false,退化为路径启发式 (这是降低打扰的策略,不是安全边界)。
required 模式下探测不到沙箱会明确报错,适合把「必须隔离」作为合规要求的部署。
8.5 命令白名单
createDefaultEngine({
workspace,
mode: Mode.AUTO,
allowedCommands: ["git status", "git diff", "ls", "cat", "pnpm test"],
});规则:
- 前缀匹配,但必须在词边界结束:
ls不会放行lsof; - 含 shell 运算符(
; & | > < \$( ( \n)的命令**一律不自动放行**, 避免ls; rm -rf /` 绕过; - 会话内用户选择「总是允许该命令」后,按可执行程序名记忆,只对简单命令生效。
8.6 安全边界清单
- 不要把
workspace设为/、用户主目录或包含密钥的目录; - 默认用
Mode.INTERACTIVE,不要为了「自动化体验」默认AUTO; - 多租户场景每个租户独立
workspace、toolResultDir、FileLogHook目录、 MCP 配置; - MCP 工具默认
requiresApproval: true,不要整体关闭审批; logs/logDir会记录 LLM 请求内容,按数据分级决定是否开启、保留多久;- 工具返回的网页/文件内容是不可信数据,不要把它当作指令执行(引擎的提示词 已包含相关约束,但业务侧仍应做输出校验)。
9. 事件流完整参考
9.1 结构
interface WsEvent {
type: EventType;
data: Record<string, unknown>; // 字段随类型变化
}run() 返回 AsyncGenerator<WsEvent>。必须消费完(或用 break 后让生成器 被回收),否则引擎不会继续推进。典型顺序:
turn_start
├─ (compacting → compacted)? # 上下文接近阈值时
├─ llm_request
├─ assistant_delta / reasoning_delta* # 流式增量
├─ assistant_message # 本轮完整回复
├─ tool_proposed
├─ (permission_required → approver)? # 需要审批时
├─ tool_started
├─ tool_finished
├─ iteration_end
└─ 回到 llm_request(还有工具结果要喂回模型时)
turn_end异常路径:error 后仍会补一个 turn_end;中断路径:interrupted 后补 turn_end。消费方应以 turn_end 作为一轮的状态机终态,而不是以流结束。
9.2 事件类型与字段
| 事件 | data 关键字段 | 何时出现 |
|---|---|---|
turn_start | taskId, input, attachmentCount?, attachments? | 每轮开始 |
assistant_delta | text, reqId, taskId | 模型流式文本增量 |
reasoning_delta | text, reqId, taskId | 推理过程增量(部分模型) |
assistant_message | text, reasoning, tool_calls(工具名数组), usage?, reqId, taskId, logTime | 一次 LLM 回复结束 |
tool_proposed | reqId(=工具调用 ID), name, arguments, taskId | 模型提议调用工具 |
permission_required | taskId, reqId, name, arguments, reason, category | 需要审批(随后调用 approver) |
tool_started | taskId, reqId, name, startTime? | 工具开始执行 |
tool_finished | taskId, reqId, name, status, reason?, duration?, result_preview? | 工具结束 |
iteration_end | iteration, taskId | 一次迭代(LLM + 工具)结束 |
turn_end | status, iterations, produced_files, taskId | 一轮结束(必出) |
error | error, error_type, retriable, iteration, reqId, logTime, taskId, stack? | 引擎错误 |
interrupted | iterations, reqId, taskId | 用户中断 |
compacting | taskId, trigger, tokens, estimated, contextWindow, thresholdPct, thresholdTokens, outboundMessages | 开始压缩 |
compacted | reqId, taskId, method, messagesBefore, messagesAfter, boundaryIndex?, model?, summaryPreview?, keptUserMessages?, droppedUserMessages? | 压缩完成 |
queue_steering | text, taskId, attachments? | 排队的补充指令被注入 |
llm_request | reqId, taskId, logTime, model, msgCount, toolCount, compact? | 每次 LLM 请求前(审计用) |
ask_user_question | taskId, question, options, allowText, multi | 模型调用 ask_user |
plan_proposed | plan | 模型调用 propose_plan |
directory_requested | reason, path, writable | 模型调用 request_directory |
llm_retry | attempt, max, reason, waitMs, iteration, reqId | 瞬时网络错误自动重试 |
session_named | name, source | 枚举保留;当前由 CLI/server 在命名后自行广播,TurnEngine 不产出 |
tool_finished.status 取值:
| status | 含义 |
|---|---|
ok | 执行成功 |
error | 工具抛异常或返回错误 |
denied | 权限拒绝或用户拒绝 |
interrupted | 执行中被中断 |
turn_end.status 取值:
| status | 含义 |
|---|---|
completed | 模型不再请求工具,正常结束 |
max_iterations_exceeded | 达到 maxIterations |
interrupted | 用户中断 |
error | LLM/引擎错误终止 |
9.3 消费示例:转成 SSE
import type { WsEvent } from "@zqagent/core";
function toSse(event: WsEvent): string {
return `event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`;
}
app.post("/sessions/:id/messages", async (req, res) => {
const engine = sessions.get(req.params.id)!.engine;
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
for await (const event of engine.run(String(req.body.text))) {
res.write(toSse(event));
}
res.end();
});注意:SSE 断线不会自动中止引擎。客户端断开时应调用 engine.requestInterrupt(), 否则引擎会继续跑完并消耗 token。
9.4 错误与重试
error.retriable === true:可尝试engine.retry()(上一轮以错误结束、且末尾 是可重试错误时才有效)。- 429/5xx 由 OpenAI SDK 自身重试;瞬时网络错误引擎会自动重试
NETWORK_RETRY_MAX(默认 2)次,退避基数NETWORK_RETRY_BASE_MS(默认 800ms), 期间发llm_retry。 - 上下文溢出会自动压缩后重试,不对外报错;压缩也失败时才报错并标记可重试。
10. 模型 Provider
10.1 OpenAIProvider
import { OpenAIProvider } from "@zqagent/core";
const provider = new OpenAIProvider({
apiKey: process.env.OPENAI_API_KEY, // 缺省读同名环境变量
baseURL: process.env.OPENAI_BASE_URL, // 任意 OpenAI 兼容端点
logDir: "/var/log/zqagent/llm", // 可选:请求审计日志
});行为要点:
- Responses API 优先,失败按错误类型降级到 Chat Completions:
- 端点不支持(404/405 等)→ 降级并按模型缓存;
- 参数不兼容(部分兼容网关)→ 仅本次降级;
- 401/429/5xx/网络错误 → 不降级,原样抛出。
- 环境变量
OPENAI_FORCE_API_MODE=responses|chat可强制模式。 - 模型名或 baseURL 含
deepseek时,Responses 路径自动注入服务端web_search内置工具,并过滤本地同名工具。 logDir按日期分目录、按reqId写请求/响应审计文件;包含消息内容, 按数据合规要求决定是否开启。
相关环境变量:
| 变量 | 作用 |
|---|---|
OPENAI_API_KEY | API Key |
OPENAI_BASE_URL | 兼容端点 |
OPENAI_MODEL | 默认模型 |
OPENAI_REASONING_EFFORT | low / medium / high |
OPENAI_FORCE_API_MODE | responses / chat |
OPENAI_MAX_RETRIES | SDK 重试次数 |
OPENAI_TIMEOUT_MS | 请求超时 |
OPENAI_STREAM_IDLE_MS | 流空闲看门狗 |
SOCKS5_PROXY | LLM 与联网工具走 SOCKS5 代理 |
10.2 自定义 Provider
引擎只依赖两个方法(packages/core/src/providers/OpenAIProvider.ts):
// 注意:StreamOpts 目前未从包根导出;实现自定义 provider 时,
// 按下面用到的字段自行声明即可(reqId / logTime 必填,其余可选)。
interface ProviderClient {
stream(
model: string,
messages: Message[],
tools: ToolSchema[],
opts: StreamOpts,
): Promise<AsyncIterable<StreamChunk>>;
chat(
model: string,
messages: any[],
options: RequestOptions,
opts: StreamOpts,
params?: { maxTokens?: number },
): Promise<string | null>;
}StreamChunk:
interface StreamChunk {
content: string | null;
reasoning: string | null;
toolCalls: ToolCall[] | null; // 仅流结束 chunk 给完整列表
usage: TokenUsage | null;
finishReason: string | null;
}实现自定义 provider 时最关键的两点:
- 最后一个 chunk 必须带
toolCalls或finishReason,否则引擎认为流没有正常结束; - 收到
opts.signal的 abort 要立即停止读取并抛出/结束流,否则「停止」按钮无效。
chat() 主要用于会话命名等轻量请求;即使返回 null 也不影响主流程。
10.3 推理强度
createDefaultEngine({
workspace,
reasoningEffort: "high", // low / medium / high
});透传给 provider;OpenAI 系模型生效,DeepSeek 自动跳过(它用 thinking 开关, 不支持级别控制)。
11. 生命周期钩子与持久化
11.1 EngineHook 全部回调
| 回调 | 参数 | 用途 |
|---|---|---|
onTurnStart | input | 一轮开始 |
onTurnEnd | status, iterations, producedFiles | 一轮结束 |
onAssistantDelta | text | 流式文本 |
onReasoningDelta | text | 流式推理 |
onAssistantMessage | text, toolCalls, usage? | 完整回复(计费常用) |
onToolProposed | name, args | 工具提议 |
onToolStarted | name | 工具开始 |
onToolFinished | name, status, resultPreview | 工具结束 |
onPermissionRequired | name, reason | 需要审批 |
onIterationEnd | iteration | 迭代结束(检查点常用) |
onCompacting / onCompacted | info / text, info | 压缩 |
onError | error, errorType, info? | 错误(含 retriable) |
onRetry | attempt, max, reason | 网络自动重试 |
onInterrupted | iterations | 中断 |
onEvent | type, data | 未匹配事件的兜底 |
钩子是同步回调,引擎不会 await。因此:
- 不要在 hook 里做长时间阻塞操作(会卡住引擎);
- hook 抛出的异常会被引擎吞掉(
_emitHook内部 try/catch),不会影响主流程; - 需要背压/顺序保证的数据,请消费
run()的事件流,而不是 hook。
11.2 组合多个 Hook
import { CompositeHook, ConsoleHook, FileLogHook } from "@zqagent/core";
const hook = new CompositeHook([
new ConsoleHook(),
new FileLogHook(sessionId, "/var/lib/myapp/sessions"),
]);createDefaultEngine({ vcs }) 内部也用 CompositeHook 把 VcsHook 并联进 调用方自己的 hook,不会覆盖它。
11.3 FileLogHook:文件级持久化与恢复
import { FileLogHook } from "@zqagent/core";
const hook = new FileLogHook(sessionId, stateDir);
// 写入:
// <stateDir>/<sessionId>.jsonl 事件日志(JSONL)
// <stateDir>/<sessionId>_state.json 检查点
const engine = createDefaultEngine({
workspace,
messages: FileLogHook.loadState(sessionId, stateDir)?.messages,
hook,
});
// 关键节点保存状态(通常在 onIterationEnd / onTurnEnd 里调)
hook.saveState(engine, { name, nameSource: "auto", taskCount });SavedState 字段:
interface SavedState {
sessionId: string;
model: string;
messages: Message[];
compaction: Record<string, unknown> | null;
savedAt: string;
name?: string;
nameSource?: "auto" | "user";
taskCount?: number;
permissionMode?: string;
allowTools?: string[];
allowCommands?: string[];
}恢复时注意:
- 把
messages传给new TurnEngine({ messages })或createDefaultEngine({ messages }); - 恢复
permissionMode与白名单(engine.permissions.mode = ...、allowToolForSession/allowCommandForSession),否则重启后用户之前的授权会丢; FileLogHook.loadState()读不到文件时返回null,不抛错;- 多租户场景每个会话一个
stateDir,不要混目录。
FileLogHook 还提供 onPaused / onResumed 方法用于记录挂起/恢复,但它们不在 EngineHook 接口里,按需直接调用实例方法。
11.4 事件日志的用途
JSONL 事件日志是审计(谁在什么时候让 Agent 做了什么)与回放的基础; 它不是会话状态的唯一来源。文件内容变更请用第 17 章的 VCS 快照。
12. 一轮 Turn 的控制
12.1 多轮对话
import type { WsEvent } from "@zqagent/core";
async function drain(stream: AsyncIterable<WsEvent>): Promise<void> {
for await (const event of stream) {
// 渲染或忽略
}
}
await drain(engine.run("先看一下项目结构"));
await drain(engine.run("把 README 里的示例改成 TypeScript")); // 自动带上一轮上下文同一个 engine 连续调用会保留历史。要开新会话就新建 engine。
12.2 中断
// 在另一个地方(例如 HTTP DELETE / 用户点「停止」)
engine.requestInterrupt();它会:
- 置位取消标记;
- abort 在飞的 LLM 流;
- abort 正在执行的工具(
run_shell会杀掉子进程树);
随后事件流会产出 interrupted + turn_end(status: "interrupted")。 不要用「停止消费事件流」来代替中断:那样引擎不会被推进,也不会清理状态。
12.3 重试
for await (const e of engine.retry()) {
// 仅在上一轮以可重试错误结束时才会产出事件
}12.4 从挂起审批恢复
engine.resume() 用于「历史末尾有未回答的工具调用」的场景(典型是服务重启后 恢复挂起的审批):
for await (const e of engine.resume()) {
// 重新处理挂起的 tool_calls;approver 会按已 resolve 的结果立即返回
}如果没有挂起的工具调用,resume() 直接返回,不产出事件。
12.5 Steering:运行中追加指令
engine.queueSteering("顺便把测试也跑一下");
engine.queueSteering("忽略 dist 目录", attachments, refs);引擎会在迭代间隙把排队的指令作为 user 消息注入,并产出 queue_steering。 适合「Agent 正在跑长任务,用户想补充一句」的交互。尚未注入的指令可以撤销:
engine.removeSteering("顺便把测试也跑一下"); // 已注入返回 false12.6 并发约束
- 同一个 engine 同时只能有一轮在跑;
- 工具层面:低风险工具会并发执行,高风险/需审批/交互类工具串行;
- 你的服务层应对同一会话的
run()做队列化,避免两个请求同时写messages。
13. 附件
13.1 数据结构
interface MessageAttachment {
name: string;
mimeType: string; // image/* audio/* video/*
dataUrl: string; // data:<mime>;base64,<payload>
}
interface StoredAttachment {
name: string;
mimeType: string;
size: number;
path: string; // 相对会话目录,如 "attachments/xxx-0-shot.png"
}限制(packages/core/src/engine/attachments.ts):
| 常量 | 值 | 含义 |
|---|---|---|
MAX_ATTACHMENTS | 4 | 单次最多附件数 |
MAX_ATTACHMENT_BYTES | 10 MB | 单文件且合计上限 |
ATTACHMENT_DIR | "attachments" | 会话目录下的子目录 |
13.2 推荐流程:先落盘,再传引用
import {
normalizeAttachments,
saveAttachments,
type MessageAttachment,
type StoredAttachment,
} from "@zqagent/core";
// 1) 校验并归一化(非法会抛 AttachmentError)
const attachments: MessageAttachment[] = normalizeAttachments(rawInput) ?? [];
// 2) 存到会话目录
const refs: StoredAttachment[] = saveAttachments(sessionDir, attachments);
// 3) 跑一轮:base64 只在本轮发给模型,历史轮次会自动剥离
for await (const e of engine.run(text, taskId, attachments, refs)) {
// turn_start.data.attachmentCount / attachments(refs)
}StoredAttachment 随消息与事件流转,可用来在 UI 渲染缩略图;字节本体只在 <sessionDir>/attachments/。读取时用:
const { name, mimeType, size, base64 } = readStoredAttachment(sessionDir, ref.path);安全注意:
readStoredAttachment只接受attachments/<file>形式,拒绝..与子目录;- 附件会进 LLM 请求,
logDir审计日志会记录消息(是否记录 base64 取决于 provider 的日志实现,生产环境应评估); - 不要把用户上传的任意 HTML/脚本当作文本附件直接渲染。
14. 上下文压缩
长会话不可能无限塞进模型窗口,引擎会自动压缩。
14.1 触发机制
| 概念 | 默认 | 说明 |
|---|---|---|
| 上下文窗口 | 自动检测,回退 128K | 可用 contextWindow 覆盖 |
| 硬阈值 | 窗口的 80% | thresholdPct |
| 软阈值 | 硬阈值的 75%(≈60% 窗口) | softThresholdPct,只淘汰旧工具结果 |
| 绝对上限 | 无 | capTokens |
| 保留最近 | 约 25% | 压缩后保留的近期对话比例 |
| 摘要输出上限 | 3000 token | 内部固定 |
流程:
- 达到软阈值:淘汰旧工具结果、去重重复的
read_file,不调用 LLM; - 达到硬阈值:调用 LLM 生成摘要,产出
compacting/compacted; - API 报上下文溢出:强制压缩后重试。
14.2 事件与钩子
// engine.run(...) 的事件流中会出现:
// { type: "compacting", data: { trigger: "threshold", tokens, contextWindow, ... } }
// { type: "compacted", data: { method: "summarized" | "trimmed", messagesBefore, messagesAfter, ... } }onCompacting / onCompacted 钩子拿到同样的信息。UI 可以据此提示 「正在压缩上下文」。
14.3 可调参数与环境变量
createDefaultEngine({
workspace,
contextWindow: 200_000,
thresholdPct: 0.85,
softThresholdPct: 0.7,
capTokens: 120_000,
});| 环境变量 | 作用 |
|---|---|
COMPACTION_MODEL | 摘要使用的模型(缺省沿用当前模型) |
COMPACTION_REASONING_EFFORT | 摘要推理强度(默认 low) |
COMPACTION_ELIDE | 0 关闭旧工具结果淘汰 |
COMPACTION_DEDUP | 0 关闭重复读取去重 |
CONTEXT_WINDOW | 服务端装配时的窗口覆盖(@zqagent/server 读取) |
COMPACTION_THRESHOLD_PCT / COMPACTION_SOFT_THRESHOLD_PCT / COMPACTION_CAP_TOKENS | 服务端装配时的阈值覆盖 |
CompactionState与压缩工具函数目前没有从包根导出;第三方只需要通过engine.compactionState观察状态,或依赖事件/hook。不要深路径导入内部实现。
15. MCP 接入
MCP(Model Context Protocol)让 Agent 使用外部工具服务器。@zqagent/core 自带客户端与工具注册逻辑。
15.1 最小用法
import {
McpManager,
ToolRegistry,
defaultMcpConfigPath,
loadMcpConfigFile,
withBuiltinMcpConfigs,
} from "@zqagent/core";
const registry = new ToolRegistry();
const { configs, warnings } = loadMcpConfigFile(
defaultMcpConfigPath(workspace), // <workspace>/.agent/mcp.json
);
const manager = new McpManager(registry, withBuiltinMcpConfigs(configs), {
builtin: {
workspace,
env: process.env,
llm: { apiKey, baseURL, model }, // 内置 OCR 的多模态兜底
},
});
const { connected, failed } = await manager.connectAll();连接完成后,registry.schemas() 会自动包含 MCP 工具,模型在下一次 LLM 调用 就能看到它们(不需要重建 engine)。
15.2 配置格式
<workspace>/.agent/mcp.json 支持多种写法:
// 裸数组
[
{ "name": "filesystem", "type": "stdio", "command": ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/data"] },
{ "name": "remote", "type": "http", "url": "https://example.com/mcp", "headers": { "Authorization": "Bearer ${TOKEN}" } }
]
// 或 { "servers": [...] } / { "servers": { "name": {...} } }
// 或 Claude/Cursor 风格 { "mcpServers": { "name": { "command": "npx -y pkg", "args": ["..."] } } }type缺省时按builtin→url→command推断;command可以是字符串或数组;${VAR}/${env:VAR}会用环境变量展开;enabled: false或disabled: true禁用;- 内置预设(OCR)会在用户没有同名配置时自动追加,
{ "name": "ocr", "enabled": false }可关闭。
15.3 工具命名与审批
- 注册名格式:
mcp__<server>__<tool>(两段都做过字符清洗与截断); - 元数据:
category: "mcp"、riskLevel: "high"、requiresApproval: true; - 因此必须接 approver,否则所有 MCP 工具都会走默认兜底策略。
15.4 惰性加载(工具很多时)
new McpManager(registry, configs, {
lazy: {
enabled: true, // 或交给阈值自动判断
thresholdTools: 16,
thresholdChars: 24_000,
maxLoaded: 40,
},
});开启后只注册两个元工具:
mcp_search_tools(只读、免审批):按关键词检索工具,命中前 8 条会自动加载;mcp_call_tool(需审批):按名称调用已加载/可解析的工具。
环境变量 ZQAGENT_MCP_LAZY=1/0 可强制开关。
15.5 生命周期
await manager.connectServer("github");
await manager.refresh("github"); // 断开 → 重连 → 重新注册
manager.attach(newRegistry); // 引擎重建时迁移已加载工具
await manager.closeAll(); // 进程退出时McpManager 只做内存管理,不持久化配置:addConfig / updateConfig / removeConfig 后需要自己调用 saveMcpConfigFile()。
15.6 内置 OCR
ocr 预设暴露 mcp__ocr__recognize,支持图片/PDF 文字提取,按 macOS Vision → tesseract → 多模态 LLM 的顺序降级。builtin.llm 应传当前会话 的 provider 配置,否则第三级降级不可用。
16. 子代理与 Agent 注册表
16.1 explore 子代理
createDefaultEngine 已注册 explore 工具(packages/core/src/tools/explore.ts):
- 主代理把「研究任务」交给子代理;
- 子代理使用独立上下文、独立工具表,运行在
Mode.PLAN(硬阻断写入); - 默认只带只读文件工具、搜索、Git 只读工具;
- 默认最多 30 轮(
EXPLORE_MAX_ITERATIONS),撞上限后会再跑一轮收尾并输出报告; - 主代理只收到最终报告,子代理的中间过程不进入主上下文。
自定义子代理行为:
createDefaultEngine({
workspace,
subagentHookFactory: () => new SubagentHook("explore"), // 每个子代理一个实例
});不传 subagentHookFactory 时子代理完全静默。SubagentHook 会把子代理的工具调用 缩进打印到终端,是 CLI 的参考实现;Web 场景可以自己实现一个 hook,把子代理事件 转发到前端(注意:子代理事件不经过主 run() 的事件流)。
手动注册:
registerExploreTool(registry, {
workspace,
provider,
model,
lang: "zh",
hookFactory: () => mySubagentHook,
toolResultDir,
});16.2 AgentRegistry
AgentRegistry 是内存中的角色表,管理「名称 + 描述 + 系统提示词 + 可选模型」:
import { AgentRegistry } from "@zqagent/core";
const agents = new AgentRegistry(); // 默认内置 code / cowork
agents.add({ name: "analyst", description: "数据分析", prompt: "你是数据分析师…" });
agents.update("analyst", { model: "deepseek-chat" });
agents.switch("analyst");
const prompt = agents.buildSystemPrompt("analyst", workspace, today, "zh");
engine.setSystemPrompt(prompt); // 切换角色
engine.model = agents.currentDef.model ?? engine.model;AgentRegistry 只做内存管理,持久化(如 agents.json)由调用方实现。
17. 变更追踪与回滚
Agent 会改文件。事件日志只记录「工具意图」,不记录文件内容;要回答「这一轮到底 改了什么、怎么回滚」,用 VCS 快照。
17.1 开启
const engine = createDefaultEngine({
workspace,
vcs: { mode: "auto", sessionId: "tenant-42-session-7" },
});mode:
| 值 | 行为 |
|---|---|
off(默认) | 关闭 |
auto | 是 Git 仓库用原生模式,否则用 shadow 仓库 |
native | 用工作区 .git,但使用独立 index 与 refs/agent/*,不碰用户 HEAD/staging |
shadow | 在 <workspace>/.agent/vcs 建影子仓库 |
native / shadow 都要求本机有 git;找不到时退化为 off(写入冲突检测仍然可用)。
17.2 查询与回滚
import { SessionVcs, resolveVcsPlan } from "@zqagent/core";
const vcs = new SessionVcs(resolveVcsPlan(workspace, { mode: "auto" }), sessionId);
const turns = await vcs.listTurns(); // 只列「有改动的轮次」,序号从 1 开始
const patch = await vcs.diffTurn(2); // git show --patch
const result = await vcs.restoreTurn(2, { // 回滚到第 2 轮之后的状态
paths: ["src/app.ts"], // 可选:只回滚部分文件
dryRun: false,
});
// result: { restored, overwritten, missing }
await vcs.forget(); // 删除该会话的 refs/agent/<id>,不还原文件VcsHook 已把 onTurnStart → beginTurn()、onTurnEnd → commitTurn(status) 接好;createDefaultEngine({ vcs }) 会自动组合它。需要自己管理生命周期时, 直接 new VcsHook(vcs) 并放进你的 hook 组合。
注意:
commitTurn()返回的turn恒为 0;真正的轮次序号来自listTurns();diffTurn/restoreTurn的turn是「有改动的轮次」序号,空轮不占号;- 快照内容是
git add -A的工作区状态,包含未跟踪文件(受.agent/、.git/、node_modules/等排除规则约束)。
18. 四种集成模式与代码骨架
18.1 模式一:HTTP 服务 + SSE(最像 @zqagent/server)
关键设计:
Map<sessionId, SessionRuntime>;每个 runtime 持有 engine、事件缓冲、待审批 Promise;- 同一会话的
run()串行排队; - 审批通过
permission_required事件 + 一个待 resolve 的 Promise 实现「挂起」; - 客户端断开时
requestInterrupt()。
import {
ApprovalOutcome,
Mode,
createDefaultEngine,
type PermissionRequest,
} from "@zqagent/core";
interface Runtime {
engine: ReturnType<typeof createDefaultEngine>;
queue: Promise<void>;
pendingApproval?: {
request: PermissionRequest;
resolve: (o: ApprovalOutcome) => void;
reject: (e: Error) => void;
};
}
const sessions = new Map<string, Runtime>();
function getRuntime(sessionId: string, workspace: string): Runtime {
let rt = sessions.get(sessionId);
if (rt) return rt;
const engine = createDefaultEngine({
workspace,
mode: Mode.INTERACTIVE,
toolResultDir: `${workspace}/.agent/tool-results`,
});
const runtime: Runtime = { engine, queue: Promise.resolve() };
engine.setApprover((request) => {
return new Promise<ApprovalOutcome>((resolve, reject) => {
runtime.pendingApproval = { request, resolve, reject };
// 通过 SSE/WebSocket 把 request 推给前端;前端 POST /approve 时:
// runtime.pendingApproval?.resolve(ApprovalOutcome.ONCE)
// 超时或连接断开时:
// runtime.pendingApproval?.resolve(ApprovalOutcome.DENY)
});
});
sessions.set(sessionId, runtime);
return runtime;
}
// POST /sessions/:id/messages
// res 为 Express/Fastify 等框架的响应对象;这里只要求 write/end 两个方法
async function send(
sessionId: string,
workspace: string,
text: string,
res: { write(chunk: string): void; end(): void },
) {
const rt = getRuntime(sessionId, workspace);
const task = rt.queue.then(async () => {
for await (const event of rt.engine.run(text)) {
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`);
}
});
rt.queue = task.catch(() => {}); // 保证下一条消息不被前一条的异常卡住
await task;
}18.2 模式二:桌面应用(Electron / Tauri sidecar)
- 引擎跑在主进程或 sidecar 进程内,UI 通过 IPC 接收事件;
workspace指向用户选择的工作目录;- 审批用原生对话框;
- 文件工具与 shell 直接在本机执行,务必保留
INTERACTIVE与沙箱; - 退出前
await mcpManager.closeAll(),并hook.saveState()。
18.3 模式三:无人值守 Worker
const engine = createDefaultEngine({
workspace: jobWorkspace,
mode: Mode.INTERACTIVE,
defaultApproval: "deny", // 没人可问:默认拒绝
allowedCommands: ["npm test", "npm run build"],
});
engine.setApprover(async (req) => {
// 用业务规则做静态审批,而不是问人
if (req.toolName === "run_shell" && String(req.arguments.command).startsWith("npm test")) {
return ApprovalOutcome.ONCE;
}
return ApprovalOutcome.DENY;
});Worker 场景要额外注意:
- 每个 job 一个独立 workspace,结束后清理或归档;
- 设
maxIterations、contextWindow、capTokens上限,防止 token 失控; - 用
onAssistantMessage统计用量; - 用
onTurnEnd判断status,非completed视为失败并记录。
18.4 模式四:多租户 SaaS
必须隔离的维度:
| 资源 | 隔离方式 |
|---|---|
| 文件系统 | 每租户独立 workspace |
| 工具结果落盘 | 每租户/会话独立 toolResultDir |
| 事件与检查点 | 每会话独立 stateDir(FileLogHook) |
| Git 快照 | 每会话 sessionId → refs/agent/<id> |
| MCP | 每租户配置来源与 McpBuiltinContext.workspace |
| 审批 | 审批请求必须带租户/用户上下文,不能全局共享队列 |
| 模型凭据 | 每租户 provider 实例,不要把 key 放进 prompt/工具参数 |
不要跨租户复用 engine、registry(MCP 工具会互相污染)、permissions(白名单会串)。
19. 生产检查清单
上线前逐项确认:
装配
- [ ]
model显式配置,未配置时启动即报错; - [ ]
workspace是每租户/每任务的独立目录,不是/或主目录; - [ ]
mode默认INTERACTIVE; - [ ]
engine.setApprover()已实现,且有超时/断线兜底; - [ ]
setQuestionHandler()/setPlanHandler()/setDirectoryHandler()已实现(若使用对应工具); - [ ]
maxIterations、contextWindow、capTokens有明确上限; - [ ]
toolResultDir按租户隔离。
安全
- [ ] shell 策略与沙箱状态符合预期(
engine.permissions.sandbox); - [ ] 命令白名单不含通配/复合命令;
- [ ] MCP 工具保持审批,配置文件不包含明文密钥(用
${ENV}); - [ ] 附件大小/数量限制已在外层再校验一次;
- [ ] 审计日志的保留期与脱敏策略已确定。
可靠性
- [ ] 同一会话的
run()已串行化; - [ ] 客户端断开 →
requestInterrupt(); - [ ] 进程退出 →
mcpManager.closeAll()、hook.saveState(); - [ ] 重启恢复 → 读
SavedState.messages并还原权限模式/白名单; - [ ] 为
error.retriable提供「重试」入口; - [ ] 监控
turn_end.status分布与 token 用量。
性能
- [ ] 单会话事件不无限缓冲(SSE 直接转发,不落内存大数组);
- [ ]
logDir按日期分片并定期清理; - [ ] 大文件工具结果走落盘预算,不整段塞回模型。
20. API 速查
20.1 主入口导出(节选)
// 引擎
TurnEngine
createDefaultEngine
EventType, ApprovalOutcome
Mode, PermissionEngine
ToolRegistry, RiskClass
OpenAIProvider
// 工具注册
registerFileTools, registerReadOnlyFileTools, registerShellTool, registerSearchTool,
registerWebTools, registerGitTools, registerDiffTools, registerTodoTool, TodoList,
registerExploreTool, registerMediaTool, registerTerminalTools,
registerAskTool, setQuestionHandler,
registerPlanTool, setPlanHandler,
registerDirectoryTool, setDirectoryHandler
// 钩子与视图
ConsoleHook, SubagentHook, CompositeHook, FileLogHook,
MarkdownRenderer, approvalFields, approvalToHtmlTable, approvalToMarkdownTable
// 提示词
buildInstructions, buildEnvBlock, getPrompts, promptSets, PROMPT_LANGS
// Agent / MCP
AgentRegistry, BUILTIN_AGENTS
McpClient, McpManager, MCP_SEARCH_TOOL, MCP_CALL_TOOL,
loadMcpConfigFile, saveMcpConfigFile, readMcpConfigFile,
normalizeMcpConfig, normalizeMcpConfigs, defaultMcpConfigPath,
withBuiltinMcpConfigs, splitCommandLine, BUILTIN_MCP_CONFIGS
// 附件
MessageAttachment, StoredAttachment,
normalizeAttachments, saveAttachments, readStoredAttachment, stripAttachments,
MAX_ATTACHMENTS, MAX_ATTACHMENT_BYTES, ATTACHMENT_DIR, AttachmentError
// 沙箱
resolveSandbox, wrapWithSandbox, describeSandbox, buildMacProfile, buildBwrapArgs
// VCS
SessionVcs, VcsHook, resolveVcsPlan, agentRef, sessionIndexPath
// 会话命名:TurnEngine 内部使用 genSessionNamePrompt;
// 纯逻辑实现(shouldAutoName / sanitizeSessionName)不在主入口,
// 需要自定义命名时参考 packages/core/src/engine/sessionNaming.ts(非稳定 API)20.2 常用环境变量
| 变量 | 作用 |
|---|---|
OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL | Provider 配置 |
OPENAI_REASONING_EFFORT | 推理强度 |
OPENAI_FORCE_API_MODE | responses / chat |
OPENAI_MAX_RETRIES / OPENAI_TIMEOUT_MS / OPENAI_STREAM_IDLE_MS | 请求可靠性 |
SOCKS5_PROXY | SOCKS5 代理 |
MAX_ITERATIONS | 服务端装配时的迭代上限 |
CONTEXT_WINDOW / COMPACTION_THRESHOLD_PCT / COMPACTION_SOFT_THRESHOLD_PCT / COMPACTION_CAP_TOKENS | 服务端装配时的压缩参数 |
COMPACTION_MODEL / COMPACTION_REASONING_EFFORT / COMPACTION_ELIDE / COMPACTION_DEDUP | 压缩行为 |
NETWORK_RETRY_MAX / NETWORK_RETRY_BASE_MS | 瞬时网络错误重试 |
TOOL_RESULT_MAX_CHARS / TOOL_RESULT_SAVE_THRESHOLD / TOOL_RESULT_DIR | 工具结果预算 |
EXPLORE_MAX_ITERATIONS | 子代理迭代上限(默认 30) |
MCP_CALL_TIMEOUT_MS | MCP 工具调用超时(默认 300s) |
ZQAGENT_MCP_LAZY | MCP 惰性加载开关 |
21. 排错 FAQ
Q:工具没有执行,模型说「用户拒绝」。 approver 返回了 DENY,或设置了 defaultApproval: "deny"。检查 approver 的日志。
Q:报错「工具 X 需要审批,但装配引擎时没有注入 approver」。 没有调用 engine.setApprover()。这是默认 defaultApproval: "error" 的预期行为。 注意这个错误是 approver 抛出的,会直接从 for await 冒出来,调用方需要 try/catch(或改用 deny / 提供真正的 approver)。
Q:报错「未指定模型」。 传 model 或设置 OPENAI_MODEL。引擎不会替你选型号。
Q:报错「未设置 OPENAI_API_KEY」。 传自定义 provider,或设置 OPENAI_API_KEY。
Q:run() 一直不结束。 检查是否消费完了事件流;检查工具是否卡住(run_shell 长任务应放后台); 检查 provider 是否忽略了 opts.signal。
Q:点击「停止」后没反应。 必须调用 engine.requestInterrupt(),而不是停止读取事件流。自定义 provider 必须响应 opts.signal。
Q:重启后用户之前的「总是允许」丢了。SavedState 里有 permissionMode / allowTools / allowCommands,恢复时要 手动写回 engine.permissions。
Q:同一个工作区多个会话互相覆盖文件。 每个会话独立 engine,但文件系统是共享的。用 ConflictGuard(内置)能发现 「读后被别人改过」,要彻底隔离请给每个会话独立 workspace,或开启 vcs 审计。
Q:MCP 工具不出现。connectAll() 是异步的,需要等它完成;工具表是在每次 LLM 调用时实时读取的, 连接完成后下一轮就会出现。检查 failed 列表与配置路径。
Q:CommonJS 项目 require() 报 ERR_PACKAGE_PATH_NOT_EXPORTED。 使用 0.2.60 及以上版本;更早版本的 CJS 产物会因 ESM-only 依赖加载失败。
Q:模型请求要走代理。 设置 SOCKS5_PROXY=socks5://host:port;HTTP/HTTPS 代理暂不支持。
22. 版本、构建与发布
当前包版本:
0.3.25(packages/core/package.json)。许可:Apache-2.0。
构建:
bashpnpm --filter @zqagent/core build # tsup → packages/core/dist(ESM + CJS + d.ts) pnpm --filter @zqagent/core typecheck pnpm --filter @zqagent/core test # Vitest发布:
bash publish_npm.sh [patch|minor|major](仅 core),或仓库级的scripts/publish-all-npm.sh(shared → core → server → cli)。产物形态:
dist/index.js(ESM)、dist/index.cjs(CJS)、dist/index.d.ts、dist/index.d.cts;@zqagent/shared作为外部依赖一起安装。
升级建议:
- 锁定精确版本,升级前跑一遍集成测试;
- 关注
TurnEngineOpts/CreateEngineOpts/ 事件字段的变更(新增字段通常兼容, 删除/改名会在大版本说明); - 不要把
internal/*当作稳定 API,升级时可能变动。
相关文档
