Skip to content

本文同步自儲存庫 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、整合模式與正式環境注意事項。


目錄 ​

  1. 它是什麼,不是什麼
  2. 架構定位與套件關係
  3. 安裝與執行環境
  4. 五分鐘跑通
  5. 核心概念
  6. 裝配引擎
  7. 工具:內建與自訂
  8. 權限、審批與安全邊界
  9. 事件流完整參考
  10. 模型 Provider
  11. 生命週期鉤子與持久化
  12. 一回合 Turn 的控制
  13. 附件
  14. 上下文壓縮
  15. MCP 整合
  16. 子代理與 Agent 註冊表
  17. 變更追蹤與回滾
  18. 四種整合模式與程式碼骨架
  19. 正式環境檢查清單
  20. API 速查
  21. 排錯 FAQ
  22. 版本、建置與發布

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/coreAgent 引擎(本文主角)是
@zqagent/sharedcore 與 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 安裝 ​

bash
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 條件。推薦消費方:

jsonc
{
  "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/ 內部檔案:

ts
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+):

ts
// 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;
  }
}

執行前設定:

bash
export OPENAI_API_KEY=sk-...
export OPENAI_MODEL=gpt-4o-mini        # 或任意 OpenAI 兼容模型
node agent-demo.mjs

這一個指令碼已經涵蓋了嵌入時最重要的四件事:裝配、審批、事件消費、多回合上下文 (同一個 engine 再呼叫一次 run() 就會帶上一回合歷史)。


5. 核心概念 ​

理解下面六個物件,後面的 API 就都能對上號。

概念型別職責
EngineTurnEngine一次工作階段的執行時。持有訊息歷史、provider、工具表、權限引擎
Turnengine.run() 的一次呼叫使用者一條輸入 → 若干次 LLM/工具迭代 → 一個 turn_end
Iteration內部迴圈一次 LLM 呼叫 + 該回覆請求的全部工具執行
EventWsEvent引擎對外輸出的唯一即時通道(非同步產生器)
ToolToolSpec模型可呼叫的函式 + JSON Schema + 風險中繼資料
ApproverApprover當權限引擎判定「需要詢問使用者」時,由呼叫方決定放行/拒絕

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 ​

ts
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):

  1. 沒有傳 provider 時建立 new OpenAIProvider({ logDir });
  2. 解析沙箱方案 resolveSandbox();
  3. 建立 ToolRegistry 與 ConflictGuard,註冊 21 個內建工具(見第 7 章);
  4. 建立 PermissionEngine(工作區、模式、命令白名單、shell 策略、沙箱狀態);
  5. 沒有傳 instructions 時用 buildInstructions(agentType, workspace, today, lang) 產生系統提示詞;
  6. 組裝 TurnEngine;如果開了 vcs,用 CompositeHook 把 VcsHook 並聯進去;
  7. 依 defaultApproval 注入備援 approver。

CreateEngineOpts 全參數 ​

參數型別/預設值說明
workspacestring(必填)工作區根目錄 = 檔案與命令的權限邊界
modelstring,缺省讀 OPENAI_MODEL模型識別碼;都為空時 run() 明確報錯,不保底型號
providerProviderClient自訂 provider;缺省建立 OpenAIProvider
modeMode.INTERACTIVEDISCUSS/PLAN/INTERACTIVE/AUTO
maxIterationsDEFAULT_MAX_ITERATIONS = 800單回合最大迭代次數
reasoningEffort缺省讀 OPENAI_REASONING_EFFORTlow/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)
thresholdPct0.8硬壓縮閾值(占視窗比例)
softThresholdPct硬閾值的 75%軟閾值:只淘汰舊工具結果,不呼叫 LLM
capTokensnull硬閾值的絕對上限(token),null = 不限制
toolResultDir狀態目錄下 tool-results超預算工具結果的寫入磁碟目錄;多租戶必須依租戶隔離
logDir不記錄LLM 請求稽核日誌根目錄(依日期分目錄)
defaultApproval"error"未注入 approver 時:error/deny/allow

lang 只影響預設提示詞、工作階段命名與摘要的語言;明確傳了 instructions 時以它為準。

6.2 路徑 B:手動裝配 TurnEngine ​

ts
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 的公開屬性與方法:

ts
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
Shellrun_shell, shell_task_output, shell_task_kill執行registerShellTool
搜尋grep唯讀registerSearchTool
網路web_search, web_fetch外部registerWebTools
Gitgit_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),需要自己注入:

ts
import { 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):

ts
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;

範例:

ts
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 唯讀工具集 ​

給子代理或唯讀情境用:

ts
import { registerReadOnlyFileTools } from "@zqagent/core";
registerReadOnlyFileTools(registry, workspace); // read_file / list_files

8. 權限、審批與安全邊界 ​

8.1 決策流程 ​

PermissionEngine.evaluate() 依固定順序判斷 (packages/core/src/permissions/PermissionEngine.ts:173):

  1. 唯讀模式(DISCUSS/PLAN)→ 有副作用的操作直接拒絕;
  2. 寫入檔案路徑不在 workspaceRoot 下 → 直接拒絕;
  3. 唯讀風險(RiskClass.READ)→ 直接允許;
  4. AUTO 模式 → 有副作用的操作全部允許;
  5. shell 免審策略(shellPolicy + 沙箱)→ 可能直接允許;
  6. 命令白名單/工作階段命令白名單 → 直接允許;
  7. 工作階段工具白名單(使用者選過「總是允許該工具」)→ 直接允許;
  8. 其餘 → needsUser: true,交給 approver。

回傳結構:

ts
interface Decision {
  allowed: boolean;
  reason: string;     // 展示给用户 / 审批卡片
  needsUser: boolean; // true = 需要审批
}

8.2 執行模式 ​

ts
enum Mode {
  DISCUSS = "discuss",         // 只读,禁止写入与执行
  PLAN = "plan",               // 只读,禁止写入与执行
  INTERACTIVE = "interactive", // 有副作用操作逐次审批(推荐默认)
  AUTO = "auto",               // 有副作用操作全部放行
}

interactive 是預設值,也是面向真實使用者最安全的模式。auto 只應在隔離環境、 可丟棄的工作區或完全可信的輸入下使用。

8.3 審批回呼 ​

ts
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沙箱生效 → 免審;未生效 → 用工作區路徑判定備援
allshell 全部免審(高風險,等同只對 shell 生效的 auto)

沙箱設定:

ts
createDefaultEngine({
  workspace,
  sandbox: {
    mode: "auto",                  // auto / off / required
    extraWritable: ["/data/cache"], // 额外可写路径
  },
});
  • macOS:sandbox-exec,把 file-write* 收窄到工作區 + 暫存目錄;
  • Linux:bwrap,整機唯讀掛載 + 工作區可寫;
  • Windows:沒有等價的程序級檔案系統沙箱,enforced: false,退化為路徑啟發式 (這是降低打擾的策略,不是安全邊界)。

required 模式下偵測不到沙箱會明確報錯,適合把「必須隔離」作為合規要求的部署。

8.5 命令白名單 ​

ts
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 結構 ​

ts
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_starttaskId, input, attachmentCount?, attachments?每回合開始
assistant_deltatext, reqId, taskId模型串流文字增量
reasoning_deltatext, reqId, taskId推理過程增量(部分模型)
assistant_messagetext, reasoning, tool_calls(工具名陣列), usage?, reqId, taskId, logTime一次 LLM 回覆結束
tool_proposedreqId(=工具呼叫 ID), name, arguments, taskId模型提議呼叫工具
permission_requiredtaskId, reqId, name, arguments, reason, category需要審批(隨後呼叫 approver)
tool_startedtaskId, reqId, name, startTime?工具開始執行
tool_finishedtaskId, reqId, name, status, reason?, duration?, result_preview?工具結束
iteration_enditeration, taskId一次迭代(LLM + 工具)結束
turn_endstatus, iterations, produced_files, taskId一回合結束(必出)
errorerror, error_type, retriable, iteration, reqId, logTime, taskId, stack?引擎錯誤
interruptediterations, reqId, taskId使用者中斷
compactingtaskId, trigger, tokens, estimated, contextWindow, thresholdPct, thresholdTokens, outboundMessages開始壓縮
compactedreqId, taskId, method, messagesBefore, messagesAfter, boundaryIndex?, model?, summaryPreview?, keptUserMessages?, droppedUserMessages?壓縮完成
queue_steeringtext, taskId, attachments?排隊的補充指令被注入
llm_requestreqId, taskId, logTime, model, msgCount, toolCount, compact?每次 LLM 請求前(稽核用)
ask_user_questiontaskId, question, options, allowText, multi模型呼叫 ask_user
plan_proposedplan模型呼叫 propose_plan
directory_requestedreason, path, writable模型呼叫 request_directory
llm_retryattempt, max, reason, waitMs, iteration, reqId瞬時網路錯誤自動重試
session_namedname, source列舉保留;目前由 CLI/server 在命名後自行廣播,TurnEngine 不產出

tool_finished.status 取值:

status含義
ok執行成功
error工具拋出例外或回傳錯誤
denied權限拒絕或使用者拒絕
interrupted執行中被中斷

turn_end.status 取值:

status含義
completed模型不再請求工具,正常結束
max_iterations_exceeded達到 maxIterations
interrupted使用者中斷
errorLLM/引擎錯誤終止

9.3 消費範例:轉成 SSE ​

ts
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 ​

ts
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_KEYAPI Key
OPENAI_BASE_URL相容端點
OPENAI_MODEL預設模型
OPENAI_REASONING_EFFORTlow/medium/high
OPENAI_FORCE_API_MODEresponses/chat
OPENAI_MAX_RETRIESSDK 重試次數
OPENAI_TIMEOUT_MS請求逾時
OPENAI_STREAM_IDLE_MS串流閒置看門狗
SOCKS5_PROXYLLM 與聯網工具走 SOCKS5 代理

10.2 自訂 Provider ​

引擎只依賴兩個方法(packages/core/src/providers/OpenAIProvider.ts):

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:

ts
interface StreamChunk {
  content: string | null;
  reasoning: string | null;
  toolCalls: ToolCall[] | null; // 仅流结束 chunk 给完整列表
  usage: TokenUsage | null;
  finishReason: string | null;
}

實作自訂 provider 時最關鍵的兩點:

  1. 最後一個 chunk 必須帶 toolCalls 或 finishReason,否則引擎認為流沒有正常結束;
  2. 收到 opts.signal 的 abort 要立即停止讀取並拋出/結束流,否則「停止」按鈕無效。

chat() 主要用於工作階段命名等輕量請求;即使回傳 null 也不影響主流程。

10.3 推理強度 ​

ts
createDefaultEngine({
  workspace,
  reasoningEffort: "high", // low / medium / high
});

透傳給 provider;OpenAI 系模型生效,DeepSeek 自動跳過(它用 thinking 開關, 不支援級別控制)。


11. 生命週期鉤子與持久化 ​

11.1 EngineHook 全部回呼 ​

回呼參數用途
onTurnStartinput一回合開始
onTurnEndstatus, iterations, producedFiles一回合結束
onAssistantDeltatext串流文字
onReasoningDeltatext串流推理
onAssistantMessagetext, toolCalls, usage?完整回覆(計費常用)
onToolProposedname, args工具提議
onToolStartedname工具開始
onToolFinishedname, status, resultPreview工具結束
onPermissionRequiredname, reason需要審批
onIterationEnditeration迭代結束(檢查點常用)
onCompacting/onCompactedinfo/text, info壓縮
onErrorerror, errorType, info?錯誤(含 retriable)
onRetryattempt, max, reason網路自動重試
onInterruptediterations中斷
onEventtype, data未匹配事件的備援

鉤子是同步回呼,引擎不會 await。因此:

  • 不要在 hook 裡做長時間阻塞操作(會卡住引擎);
  • hook 拋出的例外會被引擎忽略(_emitHook 內部 try/catch),不會影響主流程;
  • 需要背壓/順序保證的資料,請消費 run() 的事件流,而不是 hook。

11.2 組合多個 Hook ​

ts
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:檔案級持久化與還原 ​

ts
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 欄位:

ts
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 多回合對話 ​

ts
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 中斷 ​

ts
// 在另一个地方(例如 HTTP DELETE / 用户点「停止」)
engine.requestInterrupt();

它會:

  1. 置位取消標記;
  2. abort 進行中的 LLM 串流;
  3. abort 正在執行的工具(run_shell 會終止子程序樹);

隨後事件流會產出 interrupted + turn_end(status: "interrupted")。 不要用「停止消費事件流」來代替中斷:那樣引擎不會被推進,也不會清理狀態。

12.3 重試 ​

ts
for await (const e of engine.retry()) {
  // 仅在上一轮以可重试错误结束时才会产出事件
}

12.4 從掛起審批恢復 ​

engine.resume() 用於「歷史結尾有未回答的工具呼叫」的情境(典型是服務重新啟動後 恢復掛起的審批):

ts
for await (const e of engine.resume()) {
  // 重新处理挂起的 tool_calls;approver 会按已 resolve 的结果立即返回
}

如果沒有掛起的工具呼叫,resume() 直接回傳,不產出事件。

12.5 Steering:執行中追加指令 ​

ts
engine.queueSteering("顺便把测试也跑一下");
engine.queueSteering("忽略 dist 目录", attachments, refs);

引擎會在迭代間隙把排隊的指令作為 user 訊息注入,並產出 queue_steering。 適合「Agent 正在跑長任務,使用者想補充一句」的互動。尚未注入的指令可以撤銷:

ts
engine.removeSteering("顺便把测试也跑一下"); // 已注入返回 false

12.6 並行限制 ​

  • 同一個 engine 同時只能有一回合在執行;
  • 工具層面:低風險工具會並行執行,高風險/需審批/互動類工具循序;
  • 你的服務層應對同一工作階段的 run() 做佇列化,避免兩個請求同時寫入 messages。

13. 附件 ​

13.1 資料結構 ​

ts
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_ATTACHMENTS4單次最多附件數
MAX_ATTACHMENT_BYTES10 MB單一檔案且合計上限
ATTACHMENT_DIR"attachments"工作階段目錄下的子目錄

13.2 建議流程:先寫入磁碟,再傳引用 ​

ts
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/。讀取時使用:

ts
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內部固定

流程:

  1. 達到軟閾值:淘汰舊工具結果、去除重複的 read_file,不呼叫 LLM;
  2. 達到硬閾值:呼叫 LLM 產生摘要,產出 compacting/compacted;
  3. API 回報上下文溢出:強制壓縮後重試。

14.2 事件與鉤子 ​

ts
// engine.run(...) 的事件流中会出现:
// { type: "compacting", data: { trigger: "threshold", tokens, contextWindow, ... } }
// { type: "compacted",  data: { method: "summarized" | "trimmed", messagesBefore, messagesAfter, ... } }

onCompacting/onCompacted 鉤子拿到同樣的資訊。UI 可以據此提示 「正在壓縮上下文」。

14.3 可調參數與環境變數 ​

ts
createDefaultEngine({
  workspace,
  contextWindow: 200_000,
  thresholdPct: 0.85,
  softThresholdPct: 0.7,
  capTokens: 120_000,
});
環境變數作用
COMPACTION_MODEL摘要使用的模型(缺省沿用目前模型)
COMPACTION_REASONING_EFFORT摘要推理強度(預設 low)
COMPACTION_ELIDE0 關閉舊工具結果淘汰
COMPACTION_DEDUP0 關閉重複讀取去重
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 最小用法 ​

ts
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 支援多種寫法:

jsonc
// 裸数组
[
  { "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 惰性載入(工具很多時) ​

ts
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 生命週期 ​

ts
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),達到上限後會再執行一回合收尾並輸出報告;
  • 主代理只收到最終報告,子代理的中間過程不進入主上下文。

自訂子代理行為:

ts
createDefaultEngine({
  workspace,
  subagentHookFactory: () => new SubagentHook("explore"), // 每个子代理一个实例
});

不傳 subagentHookFactory 時子代理完全靜默。SubagentHook 會把子代理的工具呼叫 縮排輸出到終端機,是 CLI 的參考實作;Web 情境可以自行實作一個 hook,把子代理事件 轉發到前端(注意:子代理事件不經過主 run() 的事件流)。

手動註冊:

ts
registerExploreTool(registry, {
  workspace,
  provider,
  model,
  lang: "zh",
  hookFactory: () => mySubagentHook,
  toolResultDir,
});

16.2 AgentRegistry ​

AgentRegistry 是記憶體中的角色表,管理「名稱 + 描述 + 系統提示詞 + 可選模型」:

ts
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 開啟 ​

ts
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 查詢與回滾 ​

ts
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()。
ts
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 ​

ts
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 主入口匯出(節錄) ​

ts
// 引擎
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_MODELProvider 設定
OPENAI_REASONING_EFFORT推理強度
OPENAI_FORCE_API_MODEresponses/chat
OPENAI_MAX_RETRIES/OPENAI_TIMEOUT_MS/OPENAI_STREAM_IDLE_MS請求可靠性
SOCKS5_PROXYSOCKS5 代理
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_MSMCP 工具呼叫逾時(預設 300s)
ZQAGENT_MCP_LAZYMCP 惰性載入開關

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。

  • 建置:

    bash
    pnpm --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,升級時可能變動。

相關文件

Last updated: