本文同步自儲存庫
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,升級時可能變動。
相關文件
