Skip to content

이 문서는 저장소 docs/core-integration.md에서 동기화되었습니다. 본문에서 소스 코드를 가리키는 링크는 GitHub 저장소로 이동하며, 사이트 내에서 읽을 수 있는 관련 페이지는 사이트 내 링크로 변경했습니다.

@zqagent/core를 타사 시스템에 통합하기: 전체 통합 매뉴얼 ​

중국어(간체) · 현재 저장소 0.3.25 기준

이 문서는 AI Agent 기능을 자체 시스템에 내장하려는 팀을 대상으로 합니다. 여러분의 시스템은 웹 서비스, 데스크톱 애플리케이션, 작업 큐 Worker, 또는 이미 세션/사용자/과금 체계를 갖춘 제품일 수 있습니다. 목표는 CLI를 설치하지 않고 zqagent의 server를 시작하지 않은 채, @zqagent/core를 라이브러리로만 사용하여 「모델 + 도구 + 권한 + 이벤트 스트림 + 영구 저장」이라는 Agent 기능 일체를 확보하는 것입니다.

먼저 최소 예제만 실행해 보고 싶다면 embedding.md와 examples/embed-basic를 참고하세요. 이 문서는 그 완전판으로, 모든 공개 API, 통합 모드, 프로덕션 주의 사항을 다룹니다.


목차 ​

  1. 무엇인가, 무엇이 아닌가
  2. 아키텍처 위치와 패키지 관계
  3. 설치 및 실행 환경
  4. 5분 안에 실행하기
  5. 핵심 개념
  6. 엔진 구성
  7. 도구: 내장 및 사용자 정의
  8. 권한, 승인 및 보안 경계
  9. 이벤트 스트림 전체 참조
  10. 모델 Provider
  11. 라이프사이클 훅과 영구 저장
  12. 한 턴 제어
  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 전용 의존성 때문에 로드에 실패합니다).

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. 5분 안에 실행하기 ​

다음은 완전히 실행 가능한 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_EFFORT 읽기low / medium / high; DeepSeek는 자동 건너뜀
allowedCommands[]자동 모드에서 자동 허용할 명령 접두사
shellPolicy"approve"approve / sandbox / workspace / all
sandbox기본값 auto{ mode, extraWritable, ... }, 8.4 참고
vcs{ mode: "off", sessionId: "default" }턴마다 Git 스냅샷, 17장 참고
instructionsbuildInstructions로 생성시스템 프롬프트 재정의
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 / 데스크톱: 사용자가 UI에서 버튼을 클릭할 때까지 블로킹 대기
  • 웹 서비스: 요청을 Promise에 넣고 클라이언트에 승인 카드를 푸시한 뒤, 콜백을 받으면 resolve — 이것이 @zqagent/server의 「일시 중지형 승인」입니다.
  • 무인 Worker: 허용 목록에 따라 자동으로 ONCE를 반환하고 나머지는 DENY(또는 defaultApproval: "allow"를 사용하되, 먼저 워크스페이스가 격리되었는지 확인).

approver를 반드시 구현해야 합니다. 기본 defaultApproval: "error"는 처음 승인이 필요할 때 오류를 발생시키며, 이는 의도된 설계입니다: 「승인 연결을 잊음 → 도구가 조용히 실행되지 않음 → 사용자는 모델이 고장 났다고 오해」를 방지합니다.

8.4 shell 무승인과 OS 샌드박스 ​

shellPolicy:

값동작
approve(기본값)허용 목록 외에는 모두 승인
sandboxOS 수준 샌드박스가 실제로 적용될 때만 무승인, 그렇지 않으면 승인
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_exceededmaxIterations 도달
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()를 호출해야 하며, 그렇지 않으면 엔진이 계속 끝까지 실행되어 토큰을 소모합니다.

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는 자동으로 건너뜁니다(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). 주 흐름에는 영향을 주지 않습니다.
  • 백프레셔/순서 보장이 필요한 데이터는 hook이 아니라 run()의 이벤트 스트림을 소비하세요.

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. 한 턴 제어 ​

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. 진행 중인 LLM 스트림을 abort합니다.
  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 트리거 메커니즘 ​

개념기본값설명
컨텍스트 윈도우자동 감지, 폴백 128KcontextWindow로 재정의 가능
하드 임계값윈도우의 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의 참고 구현입니다. 웹 시나리오에서는 직접 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(기본값)끔
autoGit 저장소이면 네이티브 모드, 아니면 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 모드 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 모드 2: 데스크톱 애플리케이션(Electron / Tauri sidecar) ​

  • 엔진은 메인 프로세스 또는 sidecar 프로세스에서 실행되고, UI는 IPC로 이벤트를 수신합니다.
  • workspace는 사용자가 선택한 작업 디렉터리를 가리킵니다.
  • 승인은 네이티브 대화상자를 사용합니다.
  • 파일 도구와 shell은 로컬에서 직접 실행되므로 반드시 INTERACTIVE와 샌드박스를 유지하세요.
  • 종료 전 await mcpManager.closeAll()하고 hook.saveState()를 호출하세요.

18.3 모드 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마다 독립 워크스페이스를 사용하고, 종료 후 정리하거나 아카이브합니다.
  • maxIterations, contextWindow, capTokens 상한을 설정해 토큰 폭주를 방지합니다.
  • onAssistantMessage로 사용량을 집계합니다.
  • onTurnEnd로 status를 판단하고, completed가 아니면 실패로 간주해 기록합니다.

18.4 모드 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 전용 의존성 때문에 로드에 실패합니다.

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: