# sema-core

> **本文件是消费者集成文档(usage),主要读者是 AI。** 内容力求精确、完整、可直接执行：确切的类型签名、所有导出、
> 事件协议、状态语义、可复制的集成代码、安全契约。看完即可独立集成，不需要再读源码。
>
> 🧭 **要理解架构 / 编译发版 / 当前 bug / 路线图与难点,或刚接手本仓?先读 [`docs/HANDOFF.md`](docs/HANDOFF.md)(唯一权威入口)。** 本 README 只覆盖"如何调用引擎"。

一个**无状态、任务化**的 AI agent 核心，推理循环最初蒸馏自 [openclaw](https://github.com/openclaw/openclaw)，现已全量重写为 first-party（design/118；血统史 `docs/vendor-history.md`）。

设计取向：**大脑在外**（你提供一个模型网关）、**执行靠工具/MCP**、**session 默认内存化**（重启丢、不重启默认留 7 天；接 durable `SessionStore`/`SessionRepo` 即升级为持久会话中心，§11）、
**MCP/skill 不持久化**（随每个任务传、跑完即弃）。一个"任务"自带它的全套配置（模型、提示词、工具、MCP、skill、限额），
跑完返回一个机器可读的结果（完成 / 受阻 / 失败 / 超时）。

- 语言/运行时：TypeScript + Node（ESM, NodeNext）。
- 外部依赖：**仅 `typebox` 与 `@modelcontextprotocol/sdk`**（design/71 P1 已删除 vendored 磁盘 skill 面，连带移除 `ignore`/`yaml` 两个旧依赖；如本文别处仍提及它们即为过时）。
- 已在真实网关（Qwen3.5-35B, vLLM, OpenAI 兼容）上全链路 live 验证。

> 📌 **当前版本状态（2026-06-29 核实，权威实时状态见 [`docs/HANDOFF.md`](docs/HANDOFF.md)）**：已干净发布 + 打 tag **`1.162.0`**（`package.json`、`git tag v1.162.0`、`npm view @sema-ai/core version` 三处一致）。**1.151~1.162 见 CHANGELOG**——含 **🖥️ 1.162 K-8 第二批 后台 shell seam**（design/103：`Bash(run_in_background)`/`BashOutput`/`KillShell` 让引擎跑真实开发循环；`BackgroundShellCapability` 接口导出单源 + NodeExecutionEnv naturalize + dispose-before-suspendVM + 三安全红线；codex+workflow 双轨复审两轮；TOB 实现归 service）、**🔴 1.161 K-8「CC full-body」第一批**（full-body profile 切片 `FULL_BODY_SYSTEM_PROMPT`/`assembleFullBodyTools` + **BREAKING 工具名对齐 CC**：read_file→Read 等，模型 tool-call 保真度，consumer 锁步翻名）、**1.160 message-identity Phase 1**（`TaskEvent.message_committed`，解锁 `compacted.preserved_segment`）+ **service [325] env-teardown 时序修**、1.155~1.159（shell §K / manual /compact / per-principal 授权轴 / P-13 / CC Plan 模式）。早段脉络：`1.104.0`（design/80 supervisor 封顶）→ `1.111~1.114`（metrics→`/bench` / ask 冒泡 / `tightenTaskSpec`）→ `1.115~1.118`（CC-parity：推理强度真切换 / S1 workflow 引擎 / S8 LLM 自助编排 / goal 模式 S2+S4）→ `1.119.0`（**改名 `@ai-only/ai-agent-core`→`@sema-ai/core`，仓库 `sema-core`**）→ `1.120~1.128`（**sema-shell 主权契约 E1–E25**，design/99）→ `1.129~1.139`（**多轮对抗 bug-hunt + vendored 硬化**，1.139 含 36 修）→ `1.135~1.143`（**2c session-sync** 云↔本地会话同步）→ `1.144~1.145`（workflow-parity CORE-1~9：`WorkflowJournalStore` scope-keyed durable resume + steerable agents `agentStream`/`onWorkflowAgentSpawn` + worktree 隔离 + per-agent activity 流；CORE-1~9 × SVC-1~5 全 SHIPPED）→ **`1.146~1.150`（当前）= sema-shell 数据契约 MF-\***（引擎↔sema-shell `/workflows` monitor↔service 的可观测数据契约，与 service 协调：MF-24 `TaskResult.stats.humanReview.gates[]` 权限门/denial 账本 + 脱敏边界 `src/core/arg-summary.ts` / MF-10 `TaskEvent.task_progress` subagent usage tick / MF-25 `TaskResult.model` 回声 effective model / MF-18 `compacted.trigger` / MF-14 `CheckpointSummary.contentKind` / MF-W WorkflowRun monitor header+活动 arg；MF-Fleet 由 service 进程内 live 聚合，core 只产 fact/signal 不建 durable 父系血缘）。**最近建造线 = sema-shell 数据契约（MF-\*/MF-Fleet）+ P2 design-review / workflow-parity（design/97）/ sema-shell 主权契约（design/99）/ 2c session-sync**；**已封顶降为基座**：CC-parity 编排 + 推理强度 + goal + S8（design/96/97/98，§4.15）· design/80 Supervisor + 价值判决 GOAL 达成（§4.14）。**当前焦点**（快速移动，真相=`git log`+`团队/`通道+memory）= sema-shell 数据契约 loop（MF-\*/MF-Fleet）+ P2 design-review，与 service 协调。默认仍是 `DEFAULT_MAX_TURNS=100` / `DEFAULT_MAX_SUSPENDS=5`（保守安全网，长自治任务由 leader 显式设上限）。`npm test` 全绿（以实跑为准；**3351 passed / 25 skipped**，每发布版都跑全维度）。design/74（资源切片 suspend/resume，长自治跑）已**落地并 make-real LIVE 验证**。

---

## 目录

1. [要求与安装](#1-要求与安装)
2. [60 秒集成](#2-60-秒集成)
3. [核心概念](#3-核心概念)
4. [API 参考](#4-api-参考)
5. [流式事件协议（SSE）](#5-流式事件协议sse)
6. [任务状态语义](#6-任务状态语义)
7. [安全模型](#7-安全模型必读)
8. [多 agent：subagent 与 team](#8-多-agentsubagent-与-team)
9. [Session、内存化与自动压缩](#9-session内存化与自动压缩)
10. [模型网关接入](#10-模型网关接入)
11. [扩展点](#11-扩展点)
12. [示例与自检](#12-示例与自检)
13. [vendored 内核与许可](#13-vendored-内核与许可)
14. [已知限制 / TODO](#14-已知限制--todo)

---

## 1. 要求与安装

- Node ≥ 20（用到全局 `fetch`、`zlib.crc32`；开发机实测 Node 22/24）。
- 模型网关：一个 **OpenAI 兼容**的 `/v1/chat/completions` 端点（流式 + function calling）。

```bash
npm install          # 安装 typebox / @modelcontextprotocol/sdk 等
npx tsc --noEmit     # 类型检查（应为 0 错误）
npm run smoke        # 用 mock brain 跑 e2e（无需网关）
```

**作为 npm 包安装**（公开发布在 npmjs，scope `@sema-ai`，无需任何 token）：

```bash
npm install @sema-ai/core
```

导入：`import { Runner, createOpenAIBrain } from "@sema-ai/core";`。
包发布的是编译产物 `dist/`（`.js` + `.d.ts`）；在本仓开发时直接用 `tsx` 跑 `src/`。

---

## 2. 60 秒集成

```ts
import { Runner, createOpenAIBrain, createSqlTool, pgQuery, type Model } from "@sema-ai/core";
import { Type } from "typebox";

// 1) 定义模型（指向你的网关）
const model: Model = {
  id: "Qwen3.5-35B", name: "Qwen3.5-35B",
  api: "openai-completions", provider: "local-vllm",
  baseUrl: "http://172.30.228.10:8000/v1",     // 你的网关，结尾不带 /chat/completions
  reasoning: true, input: ["text", "image"],
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
  contextWindow: 262144, maxTokens: 4096,
};

// 2) 建 Runner（brain = 外部大脑；models = ref 目录）
const runner = new Runner({
  brain: createOpenAIBrain(),                   // 直连 model.baseUrl
  models: { qwen: model },
});

// 3) 跑一个任务
const res = await runner.runTask({
  objective: "查 orders 表里华东区(East)的总销售额，并说出数字。",
  model: "qwen",
  sessionId: "chat-42",                         // 传则续聊，不传则新建
  tools: [
    createSqlTool({
      allowTables: ["orders"],
      query: pgQuery(readonlyPgPool),           // 你的【只读】连接池
    }),
  ],
});

console.log(res.status);   // "completed" | "blocked" | "failed" | "timeout"
console.log(res.result);   // 最终文本
console.log(res.sessionId);// 用于下一轮续聊
```

流式版（OA 聊天框逐字输出）见 [§5](#5-流式事件协议sse)；HTTP 服务见 [§4.10](#410-createtaskserver--http--sse-服务)。

---

## 3. 核心概念

| 概念 | 是什么 | 对应类型/函数 |
|------|--------|--------------|
| **Brain（大脑）** | 外部 LLM。一个流式补全函数。你提供它，全局共用。 | `Brain` / `createOpenAIBrain` |
| **Task（任务）** | 一次自带全套配置的运行单元（模型/提示/工具/MCP/skill/限额）。 | `TaskSpec` → `TaskResult` |
| **Runner** | 持有 brain + 模型目录 + 内存 session 仓库；执行任务。 | `Runner` |
| **Session** | 一段对话的工作记忆。**内存**存储，传同一个 `sessionId` 即续聊。 | `TtlSessionStore` |
| **Tool（工具）** | 模型能调用的原生函数（typebox 参数 + execute）。 | `defineTool` / `ToolSpec` |
| **MCP** | 任务级临时连接的 MCP server，工具被 materialize，跑完断开。 | `materializeMcpTools` / `McpServerSpec` |
| **ExecutionEnv** | "在哪执行 shell/文件"的抽象。本包默认 stub（不需要 shell）。 | `StubExecutionEnv` |

**一句话数据流**：`TaskSpec` →（Runner 装配 harness：注入 brain、工具、内存 session）→ agent 循环（模型↔工具）→ 自动压缩 → `TaskResult`。

---

## 4. API 参考

所有导出见 `src/index.ts`。下面是确切签名与语义。

### 4.1 `Runner`

```ts
class Runner {
  constructor(deps: RunnerDeps);                 // 含可选 sessionStore(持久化见 §4.11)
  readonly sessions: SessionStore;

  runTask(spec: TaskSpec): Promise<TaskResult>;
  runTaskStream(spec: TaskSpec): TaskStream;   // AsyncIterable<TaskEvent> & { result(): Promise<TaskResult> }
}

interface RunnerDeps {
  brain: Brain;
  models?: Record<string, Model>;              // 解析字符串 ModelRef 用
  roles?: ModelRoles;                          // 角色→模型默认表(见 §4.12)
  sessionStore?: SessionStore;                 // 持久化会话(见 §4.11)
  // … memoryStore / promptProvider / toolPolicy / allowImageUrl / onError
}

// 便捷一次性：runTask(spec, deps)  —— 内部建临时 Runner
function runTask(spec: TaskSpec, deps: RunnerDeps): Promise<TaskResult>;
```

- `runTask` = 跑到结束返回结果。`runTaskStream` = 边跑边吐事件、`.result()` 取最终结果。
- 同一个 `Runner` 实例共享内存 session 仓库（续聊、TTL 都在这里）。

### 4.2 `TaskSpec`（任务输入）

```ts
interface TaskSpec {
  taskId?: string;                  // 缺省用 sessionId
  objective: string;               // ★ 本轮指令 / 用户消息
  images?: ImageInput[];           // 图文输入（模型需支持 vision）
  sessionId?: string;              // 传=续聊；不传=新建（隔离）
  model?: ModelRef;                // "qwen" 这类 ref（查 RunnerDeps.models）或完整 Model 对象；省略则按 modelRole 解析(见 §4.12)
  modelRole?: ModelRole;           // model 省略时解析哪个角色，默认 "default"
  roles?: ModelRoles;              // 按任务/场景覆盖 RunnerDeps.roles
  compactionModel?: ModelRef;      // 压缩用的便宜模型（可选；不传则用 summarize 角色，若配置了）
  thinking?: ThinkingLevel;        // "off"|"minimal"|"low"|"medium"|"high"|"xhigh"|"max"
  systemPrompt?: string;           // 预设系统提示
  tools?: ToolSpec[];              // 原生工具
  mcp?: McpServerSpec[];           // 任务级 MCP（跑完即弃）
  skills?: SkillSpec[];            // 任务级 skill（对象，不落盘）
  limits?: { maxTurns?: number; timeoutSec?: number };
  enableBlockedReport?: boolean;   // 注入 report_blocked 工具，默认 true（见 §6/§7）
  shellGate?: "off"|"always"|"classify"; // design/80 D-2：hands `bash` 的不可逆门控原则（见 §4.14/§7）。默认 "off"
  signal?: AbortSignal;            // 外部取消：abort 即停（取消 brain 请求 + 释放审批闸）
  getApiKeyAndHeaders?: (model: Model) =>
    Promise<{ apiKey: string; headers?: Record<string,string> } | undefined>;
}

type ImageInput =
  | { data: string; mimeType: string }   // base64（无 data: 前缀）
  | { url: string };                      // runner 端 fetch→base64

type ModelRef = string | Model;
```

### 4.3 `TaskResult`（任务输出，机器可读）

```ts
interface TaskResult {
  taskId: string;
  sessionId: string;               // ★ 下一轮续聊用
  status: "completed" | "blocked" | "failed" | "timeout";
  result: string;                  // 最终 assistant 文本（已 trim）
  blockedReason?: string;          // status==="blocked" 时：为何卡住
  errorMessage?: string;           // status==="failed"/"timeout" 时
  stats: {
    turns: number; tokens: number; costUsd?: number;
    promptTokens?: number;           // 跨 provider 归一的输入 token(命中率分母)
    cachedTokens?: number;           // 前缀缓存命中的 token
    cacheWriteTokens?: number;       // 写入缓存的 token(Anthropic cache_creation)
    cacheHitRate?: number;           // cachedTokens/promptTokens ∈[0,1]，成本关键，按会话趋势监控
    nested?: { tokens; turns; tasks; costUsd? };  // 子 agent 委派用量
    costBreakdown?: {                // design/80 D-E-core：LLM 派生成本的财务分类（micro-USD），供监督面分摊
      llmRootMicroUsd;               //   根 agent LLM 成本 = costMicroUsd − compactionMicroUsd
      nestedSubagentMicroUsd;        //   委派子 agent（nested）LLM 成本
      memoryConsolidationMicroUsd;   //   任务后记忆固化 LLM 成本（独立预算外行，异步补填）
      compactionMicroUsd;            //   任务内压缩 LLM 成本（含在 costMicroUsd 内）
    };                               //   恒等式：llmRoot+nestedSubagent+compaction === costMicroUsd（infra 轴由 service 补）
  };
}
```
> **缓存可观测**:`cacheHitRate` 是前缀缓存命中率(成本命脉)。多轮任务(≥2)+ 大提示(≥8000 token)+ 命中 <15%
> 时,core 经 `onError({ phase:"prompt-cache" })` **告警**(前缀不稳/被污染的信号);单轮冷启动不会误报。
> 按任务记录 `cacheHitRate`、按 `sessionId` 聚合成趋势即可做你们的成本看板/告警。
> - **命中率分母按 `model.api` 自动判 family**(Anthropic 的 input 不含缓存、OpenAI 含)。若你**按 provider 路由、
>   `model.api` 与实际 brain 不符**(会导致命中率 >100%),用 `model.params.promptCacheFamily =
>   "input-includes-cached"|"input-excludes-cached"`(别名 `"openai"`/`"anthropic"`)**显式覆盖**;core 还会在
>   `cachedTokens>promptTokens` 时 clamp 到 1 并经 `onError` 报错,不会给你荒谬值。

状态判定见 [§6](#6-任务状态语义)。

### 4.4 `Brain` 与 `createOpenAIBrain`

```ts
interface Brain {
  stream: StreamFn;                                  // 流式补全（核心）
  complete?: (model, ctx, options?) => Promise<...>; // 非流式；缺省由 stream 派生
}

function createOpenAIBrain(config?: OpenAIBrainConfig): Brain;

interface OpenAIBrainConfig {
  baseUrl?: string;                       // 缺省用 model.baseUrl
  apiKey?: string;                        // 被 options.apiKey / getApiKeyAndHeaders 覆盖
  headers?: Record<string, string>;
  fetchImpl?: typeof fetch;               // 测试/代理用
  maxRetries?: number;                    // 瞬时失败(网络/5xx/429)重试，默认 2；流开始后不重试，abort 不重试
  retryDelayMs?: number;                  // 退避基数，默认 400（指数：base*2^attempt）
  replayThinking?: boolean;               // 回灌 assistant reasoning_content（DeepSeek V4 等需要；R1/多数不要）。默认关
  connectTimeoutMs?: number;              // 建连超时：fetch 未在时限内返回响应头 → 可重试的 [network]。默认关
  firstTokenTimeoutMs?: number;           // 首-token 超时：SSE 已开但迟迟不吐第一个 delta → cancel + [network]。默认关
}
```

> 网关偶发抖动（5xx/429/连接重置）会被自动重试（仅在响应体开始流式之前）。4xx 不重试，abort 不重试。

> **分级超时（1.38.1）**：任务级 `timeoutSec` 之下补两级 brain 超时（`OpenAIBrainConfig`/`AnthropicBrainConfig`
> 共有的 `BrainTimeoutConfig`，默认关）。`connectTimeoutMs` 管"建连"（`fetch` 返回响应头前），`firstTokenTimeoutMs`
> 管"首字"（SSE 已开但网关 hang、迟迟不吐 delta —— 最常见的故障症状），两者超时都记为**可重试的 `[network]`**，
> 自动走重试 / 断路器 / failover；total 仍由 `timeoutSec` 兜底。**reasoning 模型的首个 thinking delta 也算首 token**，
> 长思考模型不会因"迟迟不出可见字"被误杀。

`createOpenAIBrain` 把统一上下文翻译成 OpenAI `/chat/completions` 请求，并把流式响应（含 **native 工具调用增量**、
`delta.reasoning`/`reasoning_content` 思考流、图片多模态）翻译回统一事件协议。**这是模型兼容性的核心**——覆盖
Qwen / Gemini / DeepSeek / GPT / OpenRouter 等所有 OpenAI 兼容网关。要接非 OpenAI 协议的网关,自己实现一个 `Brain`。

> **思考模式(reasoning)按 provider 不同**:有的 provider(DeepSeek V4)在 assistant 工具调用轮**要求回灌** reasoning，
> 否则报错/降级;有的(DeepSeek R1、多数)要丢掉。用 `replayThinking` 开关(默认丢)。截断的工具参数(`finish_reason:"length"`)
> 不会再被静默当 `{}` 执行——会以 `errorMessage` 暴露,调用方可重试。

### Anthropic 大脑(云 API):`createAnthropicBrain`

接 Anthropic Messages API(`/v1/messages`),给**云部署**用(不止本地 vLLM)。同一 `Brain` 契约,直接进 Runner,
和 failover/routing 组合(**云 Anthropic ↔ 本地 vLLM** 路由/兜底)。

```ts
import { createAnthropicBrain, createRoutingBrain, createOpenAIBrain } from "@sema-ai/core";

const brain = createRoutingBrain(
  { anthropic: createAnthropicBrain({ apiKey: process.env.ANTHROPIC_API_KEY }),
    "local-vllm": createOpenAIBrain() },
  { by: "provider", fallback: "local-vllm" },
);
```
- **提示词缓存**:`cache_control:{type:"ephemeral"}` 断点打在 system 块 + 最后一个工具(照 Claude Code);
  `cache_read/creation_input_tokens` → `usage.cacheRead/cacheWrite` → `TaskResult.stats.cachedTokens` **真实命中可见**。`cacheBreakpoints` 开关。
- **extended thinking + tool use**:thinking 块带 `signature` 捕获并**回灌**(Anthropic 硬要求)。
  思考预算 `budget_tokens` 自动钳到 `1024 ≤ budget < max_tokens`(否则 Anthropic 422);`max_tokens`
  太小时上调到能容纳"最小思考预算 + 回答余量"。用 `thinkingBudgetShare`(占 `max_tokens` 的比例,默认 0.5)
  或 `thinkingBudgetTokens`(固定值)给爱长思考的模型(如 deepseek-v4-pro)**留足回答空间**。
- **截断即报错,不再空成功**:`max_tokens`/`length` 截断且**没产出任何回答文本**(思考吃光预算)时,
  turn 以 `stopReason:"error"` + 清晰 `errorMessage` 收尾(OpenAI/Anthropic 两条路一致),不再伪装成 `completed`。
  截断的 `tool_use` 参数 JSON 同理:不带空参执行,直接报错暴露。
- 真机验证脚本:`ANTHROPIC_API_KEY=… npx tsx src/examples/anthropic-live.ts`(连跑两遍看第二遍 `cachedTokens`>0)。

> **前缀缓存对成本极关键**(见 `design/09`):提示词排版 STABLE→VARIABLE(稳定 base 在前、记忆在尾),时间戳/当前日期/id
> **绝不进前缀**,工具顺序要稳定。core 1.12 修了默认 prompt 排序;`stats.cachedTokens` + 离线 cache-guard 测试量化命中率。

**多网关故障转移**：`createFailoverBrain([primary, backup, …])` 把多个 brain 组合成主备。正常路径实时流式;
仅当某 brain **在吐任何内容之前**就失败(如网关挂)才切到下一个(不会出现重复 start/文本)。每个 brain 各自保留重试策略。
```ts
const brain = createFailoverBrain([
  createOpenAIBrain({ baseUrl: PRIMARY }),
  createOpenAIBrain({ baseUrl: BACKUP }),
]);
```

### 4.5 `defineTool` / `ToolSpec`

```ts
interface ToolSpec<TParams extends TSchema = TSchema> {
  name: string;
  aliases?: string[];                     // 旧 wire 名兼容；新工具调用应使用 name
  description: string;                    // 模型据此决定何时调用——写清楚
  label?: string;
  parameters: TParams;                    // typebox schema（Type.Object({...})）
  execute: (args: unknown, ctx: ToolExecuteContext) => Promise<ToolReturn> | ToolReturn;
}
interface ToolExecuteContext { toolCallId: string; signal?: AbortSignal }
type ToolReturn =
  | string                                                  // 文本结果
  | { content: Array<TextContent|ImageContent> | string; details?: unknown; terminate?: boolean };

function defineTool(spec: ToolSpec): AgentTool;             // ToolSpec 也可直接放进 TaskSpec.tools（内部会转）
```

- `parameters` 用 typebox。模型看到的是它序列化出的 JSON Schema。
- `execute` 抛异常 = 工具失败（循环会把错误编码成工具结果让模型看到，不会 crash 任务）。
- `terminate: true` = 提示 agent 本轮工具批次后停止。

> 注意：`TaskSpec.tools` 接受的就是 `ToolSpec[]`，无需手动 `defineTool`（Runner 内部转）。`defineTool` 用于你想拿到底层 `AgentTool`（如包一层）。

### 4.6 SQL 工具：`createSqlTool` + 适配器

```ts
function createSqlTool(opts: SqlToolOptions): ToolSpec;
interface SqlToolOptions {
  query: (sql: string) => Promise<unknown[]>;  // ★ 你注入【只读】执行器
  allowTables?: string[];                      // 表白名单（大小写不敏感）
  maxRows?: number;                            // 返回给模型的行上限，默认 100
  name?: string;                               // 默认 "query_sql"
  description?: string;
}
function validateReadOnlySql(sql: string, allowTables?: string[]): void;  // 单独可用；非法则 throw

// 适配器（零依赖，结构化类型；你传已建好的【只读】客户端）
function pgQuery(pool):    (sql:string)=>Promise<unknown[]>;   // node-postgres
function mysqlQuery(conn): (sql:string)=>Promise<unknown[]>;   // mysql2/promise（也用于 TiDB）
function sqliteQuery(db):  (sql:string)=>Promise<unknown[]>;   // better-sqlite3
```

> **TiDB**：TiDB 是 MySQL 线协议兼容的，直接用 `mysql2` 建一个**只读**连接池指向 TiDB，
> 再 `createSqlTool({ query: mysqlQuery(pool), allowTables: [...] })`。例：
> ```ts
> import mysql from "mysql2/promise";
> const pool = mysql.createPool({ host, port: 4000, user: READONLY_USER, password, database });
> const tool = createSqlTool({ query: mysqlQuery(pool), allowTables: ["orders"], maxRows: 200 });
> ```
> 务必用只读账号；TiDB 可按用户授予 `SELECT`-only 权限作为第一层保证。

安全分层（详见 [§7](#7-安全模型必读)）：①只读 DB 凭据（你的责任）②静态校验（仅 SELECT/WITH、拒多语句/注释/DML-DDL/`INTO`）③表白名单 ④行上限。

### 4.7 Gitea Issue 工具：`createGiteaIssueTool`

让 agent **给维护者开 Gitea issue**(报 bug / 提需求)。按场景注入,仓库/鉴权/默认标签由 config 固定,模型只选 `title`/`body`:
```ts
import { createGiteaIssueTool } from "@sema-ai/core";
const issueTool = createGiteaIssueTool({
  baseUrl: "https://gitea.example.com",
  owner: "AI-Only", repo: "sema-core",
  token: process.env.GITEA_ISSUE_TOKEN!,   // ★ 从环境变量取,绝不硬编码/提交
  defaultLabels: [/* Gitea 数字 label ID */], // 给所有 AI 开的 issue 打标
});
runner.runTask({ objective, model, tools: [issueTool] });
```
- `effect:"write"`(非幂等:每次开新 issue,中断**不自动重试**,避免重复 issue)。
- token **只进 `Authorization` 头**,不进 body、不进任何返回/日志文本。
- 失败(HTTP 4xx/5xx、网络)返回**可恢复的工具结果**(非抛错),模型可重试或 `report_blocked`。
- 可选 `allowModelLabels` 让模型自带 label ID(与 `defaultLabels` 合并去重)。

### 4.7 MCP：`materializeMcpTools`

```ts
function materializeMcpTools(specs: McpServerSpec[]): Promise<MaterializedMcp>;
interface MaterializedMcp { tools: AgentTool[]; dispose: () => Promise<void> }

type McpServerSpec = {
  name: string;                              // 工具会被命名空间化为 `<name>__<tool>`
  transport:
    | { kind: "stdio"; command: string; args?: string[]; env?: Record<string,string> }
    | { kind: "http"; url: string; headers?: Record<string,string> };
  allowTools?: string[];                     // 仅暴露这些工具
};
```

通常**不用直接调**：把 `McpServerSpec[]` 放进 `TaskSpec.mcp`，Runner 会在任务开始时连接、materialize 成工具，
任务结束 `dispose()` 断开。MCP 工具的 `inputSchema`(JSON Schema) 直接作为参数 schema，模型看到真实结构、循环原生校验。

### 4.8 `createSubagentTool`（委派）

见 [§8](#8-多-agentsubagent-与-team)。

### 4.9 `runTeamDiscussion`（多 agent 讨论）

见 [§8](#8-多-agentsubagent-与-team)。

### 4.10 `createTaskServer`（HTTP + SSE 服务）

```ts
function createTaskServer(opts: TaskServerOptions): http.Server;
interface TaskServerOptions {
  runner: Runner;
  resolveSpec: (body: TaskRequestBody, req) => TaskSpec | Promise<TaskSpec>;  // ★ 服务端注入 model/tools
  authorize?: (req) => boolean | Promise<boolean>;   // 返回 false → 401
  corsOrigin?: string;                               // 如 "*"
  maxBodyBytes?: number;                             // 默认 5 MiB
}
interface TaskRequestBody { objective: string; sessionId?: string; images?: ImageInput[]; [k:string]: unknown }
```

端点：
- `POST /task` → 跑到结束，返回 `TaskResult` JSON。
- `POST /task/stream` → SSE，逐条 `data: <TaskEvent JSON>\n\n`，`done` 事件后结束。

**关键安全设计**：请求体只带内容（objective/sessionId/images），**工具与模型由服务端 `resolveSpec` 注入**——
因为工具的 `execute` 函数无法、也不应通过网络传输。前端/外部 AI 不能注入可执行工具。

```ts
const server = createTaskServer({
  runner,
  corsOrigin: "https://oa.internal",
  authorize: (req) => verify(req.headers.authorization),
  resolveSpec: (body) => ({
    objective: body.objective,
    sessionId: body.sessionId,
    images: body.images,
    model: "qwen",
    tools: [ createSqlTool({ allowTables: ["orders"], query: pgQuery(readonlyPool) }) ],
    limits: { timeoutSec: 120 },
  }),
});
server.listen(8090);
```

### 4.11 `TtlSessionStore`

```ts
class TtlSessionStore {
  constructor(opts?: {
    defaultTtlDays?: number;          // 默认 7 天
    sweepIntervalMs?: number;
    repo?: SessionRepo;               // ★ 注入【持久】会话仓 → 重启/多副本续话
    evict?: "delete" | "forget";      // 空闲/release:删库 or 只清缓存(custom repo 默认 forget)
  });
  acquire(sessionId?: string): Promise<{ session: Session; sessionId: string }>;
  touch(sessionId: string): void;
  release(sessionId: string): Promise<void>;   // 一次性任务跑完想立刻丢就调它
  sweep(now?: number): void;
  get size(): number;
  dispose(): void;                              // 关定时器
}
```

`Runner` 默认自带一个(内存,重启丢)。**持久化("会话中心"):** 给 `TtlSessionStore` 传一个 durable `SessionRepo`
(如 TiDB 后端),`acquire` 会经 `repo.open` **从事件日志续话**——任意无状态 runner 重启后、或另一副本都能接上同一对话;
custom repo 下 `evict` 默认 `"forget"`(空闲只清缓存、**不删库**)。完整实现契约(CAS 乐观锁、TiDB 表草图、两条路线)见
**`design/10-会话持久化与会话中心.md`**;`test/sessions.test.ts` 有 `DbSessionRepo`/`DbSessionStore` 真跑续话。
```ts
new Runner({ brain, sessionStore: new TtlSessionStore({ repo: myTiDbSessionRepo }) }); // 持久、可横向扩
```

### 4.12 模型角色(`roles`)与 `@-model`

**角色表**:声明一次模型,主任务/压缩/子代理/team **各按角色解析**,不必每处重复模型名。角色:
`default`(主任务,`TaskSpec.model` 省略时用它)、`summarize`(压缩总结,= aider weak-model)、`subagent`、`team`、`synthesize`;
带回退链(`summarize→default`、`synthesize→team→default`)。`TaskSpec.roles` **按任务/场景覆盖** `RunnerDeps.roles`。
```ts
const runner = new Runner({
  brain, models: { strong, cheap },
  roles: { default: "strong", summarize: "cheap", subagent: "cheap" },  // 两行做成本分级
});
runner.runTask({ objective });                       // model 省略 → 走 default 角色
runner.runTask({ objective, roles: { default: "cheap" } });            // 本任务/场景覆盖
createSubagentTool({ runner });                       // 无 model → subagent 角色(→ default)
```
- `RoleSpec` 可是 `ModelRef`,也可 `{ model, thinking }` —— 角色还能带**默认 thinking 档**(显式 `TaskSpec.thinking` 仍胜)。
- 压缩:`compactionModel` 显式 > `summarize` 角色(若配置)> 主模型(不变)。
- 设计/调研(含 aider/Claude Code/LiteLLM/RouteLLM 对照、能力角色与 Layer 2 规划)见 **`design/11-模型路由与选择层.md`**。
- **两轴一起的真机示例**:`src/examples/deepseek-routing-roles.ts` —— 一个 Runner 里 `createRoutingBrain` 按 `provider`
  路由到 DeepSeek 的 Anthropic(`/v1/messages`)与 OpenAI(`/chat/completions`)两条传输,叠加 `roles` 把 pro/flash 分工
  (父任务走 pro/Anthropic、子代理走 flash/OpenAI)。`DEEPSEEK_API_KEY=… npx tsx src/examples/deepseek-routing-roles.ts`。

**`@-model`**(聊天框内联选模型):`parseModelMention(text, allowedNames)` → `{ model?, cleanedText }`。
**只返回 allowlist 内的名字、绝不内联 Model 对象**——终端用户能选*哪个已配置模型*,但注入不了任意 `baseUrl`/`apiKey`(安全边界)。
```ts
const { model, cleanedText } = parseModelMention(userMsg, Object.keys(deps.models));
runner.runTask({ objective: cleanedText, model });   // model 为空则回落 default 角色
```

### 4.13 老师模式:`runWithTeacher`(便宜学生 + 强老师升级)

**便宜小模型干主要活,卡住/答错才请教强"老师"**(escalation cascade,见 `design/12`/`design/13`)。
配 `roles: { default: 便宜学生, advisor: 强老师 }`,跑 `runWithTeacher`:

> **适用前提:任务要有"可判定的 verify 信号"**——升级只在能判断学生答错时才触发(Tier 0 同工具+同错卡住 / Tier 1 终态 rubric 质检 / `blocked`/`failed` 终态)。**不适用于开放式"完整性/质量"判断**(如 code-review"找全 bug"、创意质量):没有 oracle 知道"该有几个、漏了哪个",学生找到一个明显问题就过 verifier、却静默漏掉其余,触发机制空转(verifier 纯增本)。这类任务靠**广度 + 对抗(`team` council/debate)**而非**深度升级**——两者正交。亦提醒:**小而明确的任务上"强模型"未必更贵**(强模型输出简洁、弱模型啰嗦),"便宜学生省钱"在小任务上前提会被削弱。(service 实测反馈,2026-06,详见 `design/12` §六)
```ts
const r = await runWithTeacher(runner, { objective, tools }, {
  // 全部可选,下面是默认值
  maxEscalations: 3,            // 每次运行最多升级几次
  teacherSpendRatioCap: 0.4,    // 老师花费 ≤ 学生花费 × 此比例就停(止损)
  stuckThreshold: 3,            // 同一工具连续失败几次算"卡住"
  verifyOutput: true,           // 终态产出过一次"便宜模型 + rubric"语义质检
  takeoverAfter: 2,             // 纠正再失败几次 → 老师直接接管
});
console.log(r.status, r.result, r.escalations, r.teacherStats); // teacherStats={tokens,tasks,costUsd}
```
- **两层触发**:Tier 0 —— 重复同工具失败 → **提前中止止损**(可选便宜 stuck-monitor 二次确认);Tier 1 —— **语义 rubric 质检**(便宜模型)抓"通过了但语义错"(纯确定性触发的盲区);外加 `blocked`/`failed` 终态。
- **老师 = 隔离的 `advisor` 角色子运行**,只回 JSON `{strategy,correction,nextStep,takeover,confidence}`;**对话丢弃、只把结构化建议注回同一学生 session**(护前缀缓存),并要求学生**先用工具输出核验建议再照做**。
- **纠正→接管**:纠正多次仍不行(或 `takeover:true`)→ 老师直接做完。
- **硬护栏**:升级上限、老师/学生花费比、老师每次 ≤4 turn;全程接 `studentSpec.signal`。
- `r.escalations`(每次升级的 trigger/建议/是否接管)+ `r.teacherStats`(老师用量 `{tokens,tasks,costUsd}`,与学生 `stats` 分开)。
- 老师/helper 未配 advisor 角色时**优雅退化**到学生模型(escalation 退化为"带结构化指导重试")。真机已验(DeepSeek flash 学生 / pro 老师,pro 返回的 JSON 建议解析正常)。
- **策略仓库(可选,二期 design/14)**:配 `strategyStore` + `scope` 后,老师"奏效的通用策略"会被存下,后续**相似问题**把策略注入学生 objective(前缀缓存安全),省得再请教老师(跨会话复用)。`scope` **强制隔离**(多租户不串)。检索**高精度低召回**(每个有意义词都要命中),注入带"过去*不同*任务、可能不适用、请核验"的否定前言。`new InMemoryStrategyStore()` 默认实现;可换 TiDB/embedding 后端。
  ```ts
  const store = new InMemoryStrategyStore();
  runWithTeacher(runner, spec, { strategyStore: store, scope: "user:42" });
  ```
- **触发可调**:Tier 0 卡住检测按"同工具+**同错误**"计数(变错=进展);`escalationPolicy` 可**否决**某次升级(独立监测解耦);`verifierSamples` 让 rubric verifier 多数表决(校准)。硬护栏:`maxEscalations`、`teacherSpendRatioCap`(老师 token ≤ 学生×此比例;老师远贵于学生时调大)、老师每次 ≤4 turn。
- **真机示例**:`src/examples/teacher-mode.ts`(DeepSeek flash 学生 / pro 老师 + 策略仓库):`DEEPSEEK_API_KEY=… npx tsx src/examples/teacher-mode.ts`。

### 4.14 Supervisor 公共面(design/80 人在环监督式编排,core 侧已 shipped 至 1.104.0;已封顶降为基座)

design/80 给"人在环监督式编排"加了一组 **durable-checkpoint substrate** 公共面。**全部 ADDITIVE / 多数 INERT**——
core 只**铸造/附带**这些字段,**从不**用它们去 gate / 限额 / 抑制(结构性 ask 经 `combinePolicies` 严格胜出);由
profile / 监督面 / 部署侧去**读取并据此编排**。新增公共面如下(导出件全在 `src/index.ts`):

**① `ToolCallRequest.budget`(只读 durable 预算快照)** —— 让**无状态** `ToolPolicy.check(req)` 能针对**跨 suspend/resume
存活**的预算自限(进程内计数器每段会清零=监督升级预算缺口)。core 在 prepare 期从 durable 资源账本 + suspend 链构造,
**策略无法写回放宽**:
```ts
req.budget?: {
  resourceRemainingMicroUsd?: number;  // 本 run 还可花的 durable $（micro-USD）；undefined=无上限。
                                       //   ⚠️ 绝不 falsy-test：0=已耗尽 ≠ 无上限（只有 undefined 才是无上限）
  resourceSpentMicroUsd: number;       // 跨所有 leg 累计已花 $（micro-USD）
  suspendCount: number;               // 本 run 已 suspend 次数（durable 链）；策略可随链增长收紧
}
```
> SAFETY ask(egress / 不可逆)**永远忽略 budget、永远 ask**(不变量结构性保住,core 对安全工具照铸 `irreversible_ask`)。

**② `CheckpointGate.riskDescriptor`(监督收件箱分诊元数据,INERT)** —— 铸造在 `human` / `irreversible_ask` 升级门上的
**确定性、已脱敏**风险摘要,让监督收件箱无需重算风险即可按真严重度排序/分诊:
```ts
riskDescriptor?: {
  severity: 1|2|3|4|5;   // ToolEmu 风格分层（5 最严重），收件箱按此降序排。= riskSeverity(axes) 的纯函数
  axes: { egress?: boolean; irreversible?: boolean; shell?: boolean };  // 触发的安全轴；{} = 纯可预算 human ask
  toolName: string;
  summary?: string;      // 单行已脱敏预览（经 inlineUntrusted + 长度截断；绝不含原始密钥/全参/env）
  touchedPaths?: string[]; // fs 工具 path 参数（best-effort，每条 inlineUntrusted 截断；shell 不解析）
};
```
> INERT(trace 验证):core 从不读它去 gate/限额/抑制。`summary`/`touchedPaths` 经脱敏硬化——所有模型可控值过
> `inlineUntrusted` + 码点上限,只摘**顶层 DATA 属性标量**(嵌套对象/数组/getter 永不进收件箱),`buildRiskDescriptor`
> **结构上 TOTAL**(对抗性 / 被 hook 改写的 args 都不会让铸造崩溃或让显示输出变化)。

**③ 新增门类(`CheckpointGate.kind`)与 `ResumeOutcome` arm**:
- `irreversible_ask`(design/80 D-2):安全 mint-site ask,带 `safetyAxis { egress?, irreversible? }`(来自工具**静态** spec 标记
  `ToolSpec.egress` / `ToolSpec.irreversibility`,非可伪造的 per-call decisionReason)+ 可选 `riskDescriptor`。**非可预算**——
  budget 解析器对它永不自动批准。
- `plan_review`(design/80 D-B):**动作前**计划评审门(distinct kind,非复用 `resource_limit` 占位)。**没有待裁决的工具**——
  人评审的是**计划**而非工具调用,故此 arm **无 tool 字段**;`ResumeOutcome` 有对应 `{ kind: "plan_review" }` arm。**每个读
  `tool_approval` 字段的消费者都必须先 branch on `kind`**(契约测试钉死),计划工件本身是 profile 关注(REF)。

**④ `pendingSteer`(durable steering on a paused checkpoint)** —— 监督者可在一个**已暂停的 checkpoint** 上 park 一条
转向指令,resume 时注入(runtask)。`Checkpoint.state.pendingSteer { text: string; trusted: boolean }`;`trusted` 在
`setPendingSteer` 时从 service 的已验证 principal 检查**冻结**(operator 角色)。脏内容在 `setPendingSteer` 即被拒
(typed `steering.invalid_content`)。CheckpointStore 新增 `setPendingSteer(...)` 方法 + 导出 `validatePendingSteer(steer)`
(每个 impl 在持久化前都跑的同一套校验)。**last-writer-wins**;只写 `state.pendingSteer`,绝不动 status / 审批通道。

**⑤ `AskUserQuestion`(模型可调的结构化提问合成工具)** —— 让 agent 向**用户**提一个结构化问题。
`createAskUserQuestionTool(onQuestion?)`,工具名固定为 `"AskUserQuestion"`(内部常量 `ASK_USER_QUESTION_TOOL_NAME`,未从根导出)。
durable 模式下一次提问会**durably suspend** 任务(在工具执行前),由部署侧路由到真人/UI、答完 resume。配套导出:
`createDurableQuestionPolicy`、`QUESTION_AWAITS_RESUME` 及类型 `OnQuestion`/`AskQuestion`/`AskQuestionOption`/
`AskQuestionRequest`/`QuestionAnswer`/`QuestionAnswerItem`。无 live human 时工具仍挂载、返回 placeholder。

**⑥ 导出的辅助函数 + `Checkpoint.deadline` D-D 契约**:
- `riskSeverity(axes)` → `1|2|3|4|5`:纯函数(无 LLM / 无 clock / 无 random),收件箱排序的稳定 key。规则:
  不可逆+egress→**5** / 仅不可逆→**4** / 仅 egress→**3** / shell-gated tighten→**3** / 纯 human ask→**2**。
- `buildRiskDescriptor(input)` → `RiskDescriptor`:在门铸造处由 D-2 `safetyAxis` + shell-gate 上下文 + **SHOWN** 工具参数
  确定性派生,TOTAL by construction。
- `Checkpoint.deadline`(epoch-ms)承载 **§D-D 过期语义契约**(写在该字段 + `CheckpointStore` 接口的文档上):FACET A
  审批门=`deadline` 是 SLA resolve-deny 时间 + 更晚的 `terminalAt` 弃用 backstop;FACET B(`resource_limit`/`needs_review`/
  `plan_review`)=`deadline` 是 abandonment-TTL,只过期/reap、**绝不** resolve-deny(否则 `gate_mismatch`)。**reaper 循环 +
  gate.kind 策略 + 铸 `terminalAt` 都是部署侧(service)的活**;core 只保证 durable 字段(`deadline` + 持久化的 `gate.kind`)。
- （注:`isDurablePause(status)` 仍是 core 内部 helper、**未**从 `src/index.ts` 导出;监督 substrate 对外的辅助函数即上述
  `riskSeverity` / `buildRiskDescriptor` / `validatePendingSteer`。）

> **何处用**:这是给**自建监督客户端**(steer box / 计划评审 / decision-with-token / riskDescriptor 收件箱渲染)与
> **service 编排**(durable 审批 + steering + plan-review + worker 自修复环)消费的 substrate。core 侧已 TOP OUT;§5 序列里
> 仍属 service 的有 D-G(direct-connect client principal 加密绑定,server 侧已建+双 council+canary e2e、激活待 M2)、
> D-C(leader 监督视图=seam 注记非实现切片)。完整保证/不保证边界见 [`docs/durability-boundary.md`](docs/durability-boundary.md)。

### 4.15 CC-parity 编排 + 推理强度公共面(design/96/97/98,1.114–1.118;已封顶基座)

> 注:此后还 ship 了 **workflow-parity**(design/97 CORE-1~9,1.144~1.145:`runWorkflow`/`WorkflowRunContext` 的 steerable `agentStream`/`onWorkflowAgentSpawn` + worktree 隔离 + `WorkflowJournalStore` durable resume + per-agent activity)、**sema-shell 主权契约**(design/99 E1–E25:`tool_end.output`/`workspace_changed`/`status` 事件/`McpServerStatus`/`onElicit`/`SessionPolicyStore`)、**2c session-sync**(`SessionRepo.export/importEntries`/`FileSnapshotStore`/`StreamingImportValidator`)。这些新公共面的契约速查见 `docs/ARCHITECTURE.md` §4 能力矩阵 + `docs/SERVICE-INTEGRATION-GUIDE.md` §13/§14,完整签名以 `src/index.ts` 导出为准。
>
> 注(1.146~1.150):还 ship 了 **sema-shell 数据契约 MF-\***(引擎↔sema-shell `/workflows` monitor↔service 的可观测公共面):`TaskResult.stats.humanReview.gates[]`(MF-24 权限门/denial 账本,toolArg 经共享脱敏边界 `src/core/arg-summary.ts`)、`TaskEvent.task_progress`(MF-10 subagent usage tick,ephemeral)、`TaskResult.model`(MF-25 回声 effective/resolved model id)、`compacted.trigger`(MF-18)、`CheckpointSummary.contentKind`(MF-14)。这些数据契约的服务侧消费见 `docs/SERVICE-INTEGRATION-GUIDE.md`,完整签名以 `src/index.ts` 导出为准。

design/80 封顶后,主线转向 **CC-parity 编排 + 推理强度**。新增以下公开 API(导出件全在 `src/index.ts`):

**① Workflow 模式(`src/orchestration/workflow.ts`,design/97 S1)** —— **确定性脚本编排** thin helper,**非** LLM 动态
planner(脚本显式调 `ctx.parallel`/`ctx.pipeline`/`ctx.phase`/`ctx.agent`,而不是让模型自己规划 DAG)。
- `runWorkflow(script, deps, opts?)` → `Promise<RunWorkflowResult>`:跑一个 `(ctx: WorkflowRunContext) => Promise<...>` 脚本。
- `WorkflowRunContext` 方法:`agent(spec, opts?)`(跑一个记录在案的 agent-run,任何终态都不 throw)/ `parallel(thunks)`
  (并发,**有 barrier**=等全部;throw 的 thunk 解析为 `null`)/ `pipeline(items, ...stages)`(每项独立流过各 stage,
  **无 barrier**)/ `phase(title, body)`(可观测分组)/ `log(message)`(narrator 日志);只读字段 `runId` / `budget`。
- `WorkflowBudgetExceededError`:`opts.budget`(token 上限)耗尽时 `ctx.agent` 抛出。`MAX_WORKFLOW_ITEMS`=单次 `parallel`/
  `pipeline` 的项数上限(对齐 CC Workflow 工具)。配套类型 `WorkflowRun`/`WorkflowEvent`/`WorkflowBudget`/`RunWorkflowOptions`/
  `RunWorkflowResult` 等。

**② `/workflows` 可观测(`src/core/workflow-run-store.ts` + `src/orchestration/workflow-observe.ts`,S1b/S1c)** ——
让 workflow run 进 `/workflows` 历史 + 跨副本可见(**opt-in**,无 store 仍跑+进程内可订阅;持久化 best-effort,store throw
绝不破坏 workflow)。
- `WorkflowRunStore`(接口)+ `InMemoryWorkflowRunStore`(core 自带)+ `FileWorkflowRunStore`(`src/stores/file/workflow-run-store.ts`,
  `FileWorkflowRunStoreOptions`)。**PG 归 service**(部署 persist 轴=部署外壳实现,不在 core)。
- `summarizeWorkflowRun(run)` → `WorkflowRunSummary`、`workflowRunStoreContract`(跨后端等价契约)、`isTerminalWorkflowStatus`、
  `WorkflowRunStoreError`。
- 读取面:`listWorkflowRuns` / `getWorkflowRun` / `subscribeWorkflow`(`workflow-observe.ts`)。

**③ 推理强度(`src/brain/reasoning.ts`,design/96 S3/S5/S6)** —— 复用 vendor 7 档 `ThinkingLevel` 的 `ReasoningIntensity`
抽象 + per-endpoint clamp + effective-intensity surfacing。
- `resolveReasoning(requested, endpoint)` → `ResolvedReasoning`(`{ requested, effective, graded, clamped, format, endpoint }`):
  task 启动时由 runner 调用,发出 `reasoning.resolved` TraceEvent,让部署看到 tier 为何被降级(binary provider →`graded:false`)
  或被夹(effort-set cap →`clamped:true`)而非静默吞掉(§E honesty)。
- `resolveEffort(...)`(per-endpoint effort 档夹)、`reasoningBudgetShare` / `REASONING_BUDGET_SHARE`、`DEFAULT_EFFORT_LEVELS`、
  `isThinkingLevel` / `rankOf` / `resolveBinary`;配套类型 `ReasoningIntensity`/`ResolvedReasoning`/`ReasoningResolution`/
  `ReasoningFormat`。默认强度常量 `DEFAULT_REASONING_INTENSITY`(`src/scenarios/env.js`)。

**④ `CheckpointSummary` 新字段(`src/core/checkpoint-store.ts`,S1e)** —— 让 service inbox 一次投影即拿到展示所需,免 N+1 回取:
- `createdAt?`(epoch-ms 标量,checkpoint 创建时间,从 `Checkpoint.createdAt` 投影)。
- `toolInput?`:`tool_approval` 工具 args 的 **bounded raw 预览**(超过 `MAX_TOOL_INPUT_PREVIEW_CHARS=512` 字符截断 + `…` 标记)。
  **ECHO-ONLY**(无 gate / CAS / resume 读它——同 `riskDescriptor` 的 INERT 不变量);**脱敏归 consumer**(core 只做长度
  上限,不替 consumer 决定 redaction 策略)。

**⑤ `tightenTaskSpec`(`src/core/tighten-task-spec.ts`,config-center §10,1.114.0)** —— **tighten-only** TaskSpec merge
治理原语:把 `overrides` 层叠到 `base`,但**放松任何安全字段**(如把 `handsReadOnly` 从 `true` 降到 `false`、或两边冲突的
不可合成字段)即 `throw TaskSpecTightenError`,而不是静默选边。两件导出:`tightenTaskSpec` + `TaskSpecTightenError`。

---

## 5. 流式事件协议（SSE）

`runTaskStream(spec)` 返回 `TaskStream`（`AsyncIterable<TaskEvent>` + `.result()`）。事件类型：

```ts
type TaskEvent =
  | { type: "text_delta";      delta: string }        // 正文增量（推给前端逐字显示）
  | { type: "reasoning_delta"; delta: string }        // 思考增量（可折叠显示；不进最终结果）
  | { type: "tool_start";      toolCallId: string; toolName: string; args: unknown }
  | { type: "tool_end";        toolCallId: string; toolName: string; isError: boolean }
  | { type: "turn_end" }                              // 一轮（assistant + 工具）结束
  | { type: "compacted";       tokensBefore: number; trigger: "auto" | "manual"; preserved_segment?: { firstKeptEntryId: string } } // 压缩边界（preserved_segment = 保留尾起点的 entryId）
  | { type: "message_committed"; entryId: string; role: "user" | "assistant" | "toolResult"; toolCallId?: string } // message-identity：刚持久化的消息→其 SessionTreeEntry id（消费者据此解析 preserved_segment）
  | { type: "done";            result: TaskResult };  // 终止，携带最终结果
```
> 注：内容类事件还各带一个 ephemeral `eventId`（uuidv7，§E2 message identity）+ 子代理运行时的 `parentToolCallId`。`message_committed`（message-identity Phase 1）在每条可渲染消息（user/assistant/toolResult）持久化后发出其稳定的 `entryId`（= `SessionTreeEntry.id`，与 `compacted.preserved_segment.firstKeptEntryId` / E18 `resumeAt` / E19 `rewindFiles` 同一 id 空间）：消费者自建 `entryId→消息` 映射（tool result 用 `toolCallId` 关联、assistant/user 用流序），即可把 `firstKeptEntryId` 解析到「哪条渲染消息是压缩保留尾的起点」并画分隔线——无需 service 侧的 eventId↔entryId 旁路映射。

前端消费（SSE）：
```ts
for await (const ev of runner.runTaskStream(spec)) {
  if (ev.type === "reasoning_delta") ui.appendThinking(ev.delta);
  if (ev.type === "text_delta")      ui.appendAnswer(ev.delta);
  if (ev.type === "tool_start")      ui.showStatus(`调用 ${ev.toolName}…`);
  if (ev.type === "done")            ui.finalize(ev.result);   // {status,result,blockedReason,sessionId,stats}
}
```

`POST /task/stream` 把上面每个事件按 `data: <json>\n\n` 发出，语义一致。

---

## 6. 任务状态语义

`TaskResult.status` 的判定优先级（见 `assembleResult`）：

| status | 触发 | 附带字段 |
|--------|------|----------|
| `timeout` | `limits.timeoutSec` 到点被 abort（或异常发生在超时后） | `errorMessage` |
| `failed` | 抛异常 / 模型 `stopReason:"error"` / 被 abort（非超时，如 `maxTurns`）/ 无 assistant 消息 | `errorMessage` |
| `blocked` | 模型调用了内置 `report_blocked` 工具 | `blockedReason`（模型给的理由） |
| `completed` | 正常结束（`stopReason:"stop"`/`"toolUse"` 收尾） | — |

`blocked` 是给"外部 AI 大脑"看的关键信号：当 agent 缺信息/缺权限/请求歧义无法继续时，它会调用 `report_blocked({reason})`，
任务即以 `blocked` + `blockedReason` 返回。用 `enableBlockedReport: false` 可关掉该工具（则永远不会是 blocked）。

---

## 7. 安全模型（必读）

本系统会让 LLM 调用你提供的工具、查你的库、连 MCP。务必在集成层守住边界：

1. **工具只在服务端定义**。HTTP 层 (`createTaskServer`) 的请求体**不能**携带可执行工具——`execute` 是函数，不过网络。
   外部只传 `objective/sessionId/images`，`resolveSpec` 决定 model + tools。**不要**把任意工具集暴露给不可信调用方。
   - **工具策略 / 写审批（ToolPolicy）**：用 `TaskSpec.toolPolicy` 或 `RunnerDeps.toolPolicy` 在**每次工具调用前**拦截。
     ```ts
     toolPolicy: createApprovalPolicy({
       requireApproval: ["delete_order", "send_email"],   // 高危写操作
       approve: async (req) => await askOperator(req),     // 人工确认(可 await);拒则模型收到拒绝、不执行
       deny: ["drop_table"],
     });
     // 或 createAllowDenyPolicy({ allow:[...] }) / combinePolicies(...)
     ```
     被拒的工具 `execute` **绝不执行**,模型拿到拒绝原因可继续或上报。`approve` 可 await 操作员决定(`limits.timeoutSec` 兜底)。
   - **安全 ask 的结构性优先(design/80 D-2)**:被 `ToolSpec.egress`(外部写:push/开 PR/发送)或 `ToolSpec.irreversibility`(`always`/`maybe`)
     标记的工具,即便预算允许也**永远** ask——core 据**静态 spec 标记**(非可伪造的 per-call 值)铸 `irreversible_ask` 门并附
     `safetyAxis`/`riskDescriptor`(见 §4.14);budget 解析器对它永不自动批准。`TaskSpec.shellGate`(`off`/`always`/`classify`)
     按部署原则门控 hands `bash`:`classify` 下可证 benign 的只读单命令自动放行,其余(写/egress/管道/未知)收紧到 ask。
     **resume 必须重传 `shellGate`**(同 `toolPolicy`/`tools`),否则恢复后的后续 `bash` 不受门控。
2. **SQL 只读是你的责任**。`createSqlTool` 的静态校验（仅 SELECT/WITH、拒多语句/注释/DML-DDL/`INTO`/危险关键字、表白名单、行上限）
   是**纵深防御**，不是唯一保证。**必须**用只读 DB 账号（`pgQuery(readonlyPool)`）。把 `allowTables` 收到最小集。
3. **Prompt 注入防御**：用户输入与工具/MCP 返回的数据**是数据，不是指令**。本包内部已遵循此约定（subagent/team 的他人内容
   都以 `<statement>`/`<...>` 块"引用为数据"）。你的工具返回、SQL 结果同理——不要让其中的文本被当成可执行指令。
   子 agent 的 handoff 报告也是"证据待核验"，不能覆盖系统/用户策略。
4. **执行环境**：默认 `StubExecutionEnv`（无 shell、无文件系统）。若将来需要真执行 shell，自己实现 `ExecutionEnv` 并施加沙箱；
   不要随便给不可信任务开 shell。
5. **限额**：给每个任务设 `limits.timeoutSec` 和 `limits.maxTurns`，防失控循环与超长占用。
6. **鉴权**：`createTaskServer({ authorize })` 必接；按 token/来源校验。`corsOrigin` 收紧到你的 OA 域名。
7. **MCP**：任务级 MCP server 来自你的 `resolveSpec`（服务端），不要接受外部传入的任意 MCP 端点（可能是 SSRF/数据外泄面）。

---

## 8. 多 agent：subagent 与 team

> 两者都**显式告知模型**当前处于何种结构（subagent 的工具描述 + `SUBAGENT_SYSTEM_NOTE`；team 的角色提示块），
> 满足"需要显式提示说明"的要求。

### 8.1 subagent（委派，单向）

父任务把一个**自包含子任务**委派给隔离的子 agent；子在干净上下文里跑（看不到父对话），同步返回结构化报告。

```ts
import { createSubagentTool, SUBAGENT_SYSTEM_NOTE } from "@sema-ai/core";

const subagent = createSubagentTool({
  runner,
  model: "qwen",                       // 子模型（可与父不同，如更便宜）
  purpose: "隔离的检索/长任务/计算",
  tools: [ /* 子可用的工具子集 */ ],
  limits: { timeoutSec: 120 },
});

await runner.runTask({
  objective: "把 X 的调研委派给子 agent，拿到结果后汇总。",
  model: "qwen",
  systemPrompt: SUBAGENT_SYSTEM_NOTE,  // 可选：强化"你可以委派"的说明
  tools: [subagent],
});
```

子 agent 通过工具 `Agent({ description, prompt, subagent_type? })` 调起（旧名 `Task` 仍作为 alias 兼容），返回给父的工具结果形如：
```
[Sub-agent report · <taskName>]
status: completed
stats: turns=.. tokens=..

result:
<子的最终文本>

Verify this result before concluding. ...
```
父据此核验、续做或再委派。子默认 `enableBlockedReport: true`（子也能上报受阻）。

### 8.2 team（多 agent 讨论 + 收敛）

N 个角色化成员多轮讨论，最后由 synthesizer 合成结论。

```ts
import { runTeamDiscussion } from "@sema-ai/core";

const team = await runTeamDiscussion({
  runner,
  model: "qwen",                       // 默认模型（成员可各自覆盖）
  topic: "MVP 用单体还是微服务？",
  members: [
    { role: "求快的实用派" },
    { role: "可扩展性架构师", model: "qwen", systemPrompt: "你偏好长期可演进性。" },
  ],
  rounds: 2,                           // 默认 2
  synthesizer: { role: "facilitator" },// 默认中立主持合成
  onEvent: (e) => { /* member_start/member_end/synthesis_start/done */ },
  limits: { timeoutSec: 120 },
});

team.conclusion;   // 合成的最终结论
team.transcript;   // [{ round, role, text }, ...] 全过程
team.stats;        // { tokens, turns }
team.failures;     // 重试后仍失败的成员/合成轮数（失败会显式标记 [unavailable: ...]，不再静默）
```

> 健壮性：某成员一轮失败（网关错误/空结果）会**自动重试一次**；仍失败则该发言标记为 `[unavailable: <status>]`
> 并计入 `failures`，不会静默变成空发言污染讨论。

机制：每轮每个成员收到「`<team-discussion round=N your_role=X>` + 此前所有发言（以 `<statement>` 数据块引用）」，
只从本角色视角发言；成员每轮无状态（共享 transcript 提供连续性），编排简单、隔离。

**subagent vs team**：subagent = 父→子单向"干活"；team = 多成员互见多轮"讨论收敛"。

---

## 9. Session、内存化与自动压缩

- **内存化**：session 只在进程内存（`TtlSessionStore` 包 vendored `InMemorySessionRepo`）。**重启即丢**。
- **续聊**：传相同 `sessionId` → 复用该 session（带完整历史 + 模型/思考档位回放）。不传 → 新建隔离 session。
- **TTL 清理**：空闲 session 由 store 清理 —— 用 `TtlSessionStore({ defaultTtlDays })`（store 级，非 per-task）。一次性任务想立刻释放：`runner.sessions.release(sessionId)`。
- **自动压缩（零配置）**：每个任务结束后（idle 时）检查上下文 token，超阈值就用 LLM 生成结构化摘要替换旧历史
  （默认 `reserveTokens 16384 / keepRecentTokens 20000`）。可用 `TaskSpec.compactionModel` 指定更便宜的压缩模型。
  压缩对前端透明；发生时流式会有一个 `{type:"compacted"}` 事件。
- **自动裁剪**：超长工具/shell 输出在 vendored 层截断（保留头尾）。
- **MCP / skill 不持久化**：它们是"任务配置"的一部分，每次随 `TaskSpec` 传入，跑完即弃——不进 session 历史。

### 长期记忆（L2，跨会话）

session(L1) 是单段对话的工作记忆;**长期记忆(L2)** 让模型跨会话记住稳定的事实/偏好(用户/组织画像、约定)。
对标 Anthropic memory tool / CodeWhale user-memory:模型用 `remember` 工具存"声明性事实",下次任务自动注入系统提示。

```ts
import { Runner, InMemoryMemoryStore } from "@sema-ai/core";
const runner = new Runner({ brain, models, memoryStore: new InMemoryMemoryStore() }); // 生产用 TiDB 后端
const res = await runner.runTask({
  objective, model: "qwen", sessionId: chatId,
  memory: { scope: "user:42" },   // 开启;按用户/组织分租户
});
```
启用后:① 该 scope 的记忆作为 `<user_memory>` 块注入系统提示(稳定前缀);② 加一个 `remember` 工具(模型自存,
"durable 声明性事实,不存临时任务态")。后端 `MemoryStore` 可插拔(默认内存,生产实现 TiDB)。记忆是**事实不是命令**,
用户当前请求永远优先。

### 默认提示词 / harness 层

不传 `systemPrompt` 时,任务用内置 **`DEFAULT_SYSTEM_PROMPT`**(通用 agent 纪律:真相/行动/验证/工具使用强制/权威层级,
harvested from CodeWhale Constitution, MIT)。传了 `systemPrompt` 则原样用(非破坏)。可用 `RunnerDeps.promptProvider`
完全接管提示词组装;`DEFAULT_SYSTEM_PROMPT` 已导出供拼接你的领域提示。

> **⚠️ 自定义 promptProvider:用 `stableSystem`,别手工拼顺序。** 前缀缓存要求**稳定内容在前、易变内容(per-user
> memory / 时间戳 / id)在尾**;手工 `system()` 拼装很容易把 memory 放错(这个错从 core 一路继承到 service、OA)。
> 新口子让场景**只给稳定 base**,由 core 按纪律把 memory 追加到**最后**——结构上不可能摆错:
> ```ts
> // ✅ 推荐:只返回稳定 base,core 负责把 memory 放到最后
> const reviewer: PromptProvider = { stableSystem: ({ userSystemPrompt }) => `审查 harness…\n\n${userSystemPrompt ?? DEFAULT_SYSTEM_PROMPT}` };
> // ⛔ 旧式 system():你自己负责顺序(memory 必须放最后),Runner 会在放错时经 onError(phase:"prompt-cache") 报警
> ```
> - **`composeSystemPrompt(stable, memoryBlock?)`** — 把易变 memory 追加到尾的同一套纪律(core 内部就用它)。
> - **`assertPromptCacheFriendly(provider)`** / `analyzePromptCacheFriendliness(provider)` — 放进 CI/单测,
>   用两份只在 memory 处不同的提示词比对公共前缀,**自动揪出前缀污染**(任何把易变内容前置的写法,不止 memory)。
>   `stableSystem` provider 永远安全(core 拥有尾部),恒过。
- **可调阈值**：`TaskSpec.compaction = { enabled?, reserveTokens?, keepRecentTokens?, instructions? }`。
  `instructions` 会拼进摘要提示，用于**保住关键事实**（如 `"逐字保留所有代码/ID/名称/数字与用户显式指令"`）。
- **context-editing（工具结果清理，最轻）**：任务内上下文超窗口 ~70% 时，把**较旧 toolResult 的内容**替换成标记
  （保留轮次结构、保最近 N 条）。**请求级、非破坏**——session 仍存全量。对标 Anthropic（单清理 +29%）。`clearStaleToolResults` 可单用。
- **任务内安全网（context guard）**：若清理后仍超(~85%)，按滑动窗口裁剪（保留摘要+最近，绝不在 orphan toolResult 处开头），防顶爆。
- **空摘要保护**：若摘要模型返回空（网关抖动），**不会**用空摘要替换历史（避免上下文丢失），本轮跳过压缩。

> **触发时机**：自动压缩在**任务之间**（idle）与**任务内每个请求边界**（turn-boundary，1.88.0 起、默认开，
> `compaction.withinTask: false` 可关）都会触发；超长单任务（多日级）不再只靠 context guard 兜底。
> 内置防抖：连续压缩失败 3 次熔断、anti-thrash 再增长地板、病态配置自修复（`sanitizeCompactionSettings`）。
>
> ⚠️ **摘要质量取决于模型**。实测 Qwen3.5-35B(int4) 在**激进压缩**下会丢掉"很早之前、只提过一次、被大量无关内容淹没"的事实，
> 即便加了保留指令；split-turn 摘要也可能偏空。**这不是流程 bug**（流程正确性见单测），是模型摘要保真度的限制。
> OA 实践建议：① 用真实 256K 窗口（压缩极少触发，事实在原文里存活很久）② 调大 `keepRecentTokens`
> ③ 用更强的 `compactionModel` ④ 关键事实在对话中复述。`compaction.instructions` 能改善但不保证。

---

## 10. 模型网关接入

默认 `createOpenAIBrain()` 走 `model.baseUrl + "/chat/completions"`，标准 OpenAI 兼容（vLLM/Ollama/各类网关都适用）。

已验证网关：`http://172.30.228.10:8000/v1`，模型 `Qwen3.5-35B`（vLLM, MoE, 256K 上下文, 图文, reasoning）。要点：
- 流式 `delta.reasoning`（思考）已被解析成 `reasoning_delta` 事件；`delta.content`（正文）→ `text_delta`。
- 工具调用是标准 OpenAI 流式增量格式，brain 原生处理。
- 思考内容**不写回最终消息**（省 token、不回灌）。

`Model` 对象字段（`api` 取 `openai-completions` 等；详见 vendored `llm-core` 类型）：
```ts
interface Model {
  id; name; api; provider; baseUrl;
  reasoning: boolean; input: ("text"|"image")[];
  cost: { input; output; cacheRead; cacheWrite };   // $/百万 token，仅用于 stats.costUsd
  contextWindow: number; contextTokens?: number;     // 压缩预算用 contextTokens ?? contextWindow
  charsPerToken?: number;  // design/123 D2：结构估算系数（默认 4；新家族/代码密集 3、中文重 2-3），
                           // 贯穿压缩触发 fallback / 请求层防线 / cut-point 记账 / prompt-overhead
  maxTokens: number; params?; headers?; compat?; thinkingLevelMap?;
}
```

切模型/思考档位：在 `TaskSpec` 里改 `model`/`thinking` 即可（每任务独立）。

---

## 11. 扩展点

三个注入接口让你在不改核心的前提下替换实现：

1. **大脑** = `Brain.stream: StreamFn`。接非 OpenAI 协议网关 / 多 provider 路由 → 自己实现 `Brain`。
2. **持久化(session)** = `SessionStore` 接口(0.3.0 起一等公民)。`Runner` 接受任意 `SessionStore`:
   ```ts
   new Runner({ brain, sessionStore: myStore });   // 默认 TtlSessionStore(内存,7天TTL)
   ```
   要落"持久化中心"(如 TiDB):实现 `SessionStore`,其 `acquire` 从持久事件日志**重建** session(= wake/resume)。
   底层用导出的 `BaseSessionStorage`(extend 它,override `appendEntry`/`setLeafId` 落库) → `Session` → 自定义 `SessionStore`。
   导出件:`SessionStore` `AcquiredSession` `Session` `BaseSessionStorage` `InMemorySessionRepo` `InMemorySessionStorage`
   `uuidv7` + 类型 `SessionStorage` `SessionRepo` `SessionMetadata` `SessionTreeEntry`。
   (test/sessions.test.ts 有一个"DB rehearsal":落外部 map + 新进程 wake 续聊,即 TiDB 的接法。)

   > **崩溃一致性(1.2.0)**:若任务在一轮中途被打断(工具调用已落库、结果未落),wake 重建出的尾部会是一个**没有结果的 tool_call**
   > → 多数网关报 `tool_calls must be followed by tool messages`,该 session 之后每轮起不来。续聊 session 时 `Runner` 会自动调
   > `reconcileInterruptedSession()`:为悬空 tool_call 合成 `isError` 的 "interrupted, 结果未知" 结果,**绝不重放工具**,把"是否重做"交给模型。
   > 给工具标 `effect: "read" | "write" | "idempotent"`(`ToolSpec.effect`,默认 `write` 保守)可让该提示更精准——read/idempotent 安全重做,
   > write 需确认。自定义 `SessionStore` 也可在 `acquire` 里直接调用导出的 `reconcileInterruptedSession`。
   >
   > **跨实例并发的乐观锁(1.3.0)**:`appendEntry`/`setLeafId` 接受可选 `{ expectedLeafId }`(`SessionWriteOptions`)。`Session` 自动以
   > `entry.parentId` 为 expected leaf 传入;durable 后端把它实现成**条件写**(TiDB:`(session_id, seq)` 唯一键),两个实例 wake 同一 session
   > 并发 append 时输家抛 `SessionError("conflict")`(用导出的 `isSessionConflict(err)` 捕获后重试),**不会静默分叉**。
   > (快照 seam 等其余地基项见 `design/07`。)

   > **PostgreSQL reference adapters(1.94.0,`src/stores/pg.ts`)**:四个 durable seam 的现成 PG 实现,驱动中立
   > (传任意 `(text, params) => {rows}`,如 `(t, p) => pool.query(t, p)`),零新增运行时依赖:
   > ```ts
   > await ensurePgAgentSchema(q);                       // 幂等 DDL(可选 { memoryVector: { dimensions, pgvector: true } })
   > new Runner({
   >   brain,
   >   sessionStore: new TtlSessionStore({ repo: new PgSessionRepo(q) }),  // 会话中心(乐观锁=(session_id,seq)唯一键)
   >   checkpointStore: new PgCheckpointStore(q),        // design/45 durable suspend/resume(原子 CAS resolve)
   >   toolResultStore: new PgToolResultStore(q),        // durable offload(durable suspend 的前置)
   >   memoryStore: new PgMemoryStore(q, { embedder, pgvectorSql: true }), // 记忆:词面 / 进程内 cosine / pgvector SQL 三档可切
   > });
   > ```
   > 召回三档共用同一 cosine-distance 契约(score∈[0,2],0=最近),升级召回是配置切换不是重标定。
3. **执行环境** = `ExecutionEnv = FileSystem & Shell`。需要真跑 shell/远程 SSH 时实现它(vendored 有本地 `NodeExecutionEnv` 可参考;
   openclaw 有 SSH/Docker 实现思路)。当前 `StubExecutionEnv` 适用于"只用工具/MCP"的场景。
4. **提示词/harness(prompt)** = `PromptProvider`(组装整套 system+memory+harness)。可在 `Runner` 级注入,
   也可**每个任务热插拔**(1.1.0 起):`TaskSpec.promptProvider` 覆盖 `RunnerDeps.promptProvider`。
   → 一个 Runner 服务多场景、各自不同的整套 harness 提示词(用 `stableSystem`,core 自动把 memory 放尾,见上方 ⚠️):
   ```ts
   const reviewer: PromptProvider = { stableSystem: ({ userSystemPrompt }) => `...审查 harness...` };
   runner.runTask({ objective, model, promptProvider: reviewer });  // 仅本任务换整套提示词
   ```

### 默认即"零配置"(集成方可只传两个字段)

什么都不指定时,最简用法 `new Runner({ brain }).runTask({ objective, model })` 即可跑。安全/重型能力**全部 opt-in**:

| 维度 | 不指定时的默认 | 想要时怎么开 |
|------|----------------|--------------|
| session 持久化 | 内存 `TtlSessionStore`(7 天 TTL) | `new Runner({ brain, sessionStore })` |
| 工具审查/审批 | **无**(工具自由执行,适合长时自治跑) | `runTask({ toolPolicy })` |
| 记忆 | 关 | `new Runner({ brain, memoryStore })` + `runTask({ memory:{scope} })` |
| 提示词 | `DEFAULT_SYSTEM_PROMPT`(或任务 `systemPrompt`) | `promptProvider`(见上,可热插拔) |
| 上下文管理 | 自动压缩 + context-editing **开**(保证长跑不超窗) | `compaction` 调参 |
| 轮次/超时上限 | 都不设时套用 `DEFAULT_MAX_TURNS`(100,保守安全网——拦住工具死循环;长自治任务设自己的显式上限,如 leader 给 worker 设 10000/超时/预算)防跑飞;设任一即用你的边界 | `runTask({ limits: { maxTurns, timeoutSec } })`(`maxTurns:0` 关闭) |
| 审批闸超时 | 不挂死:Runner 把 `ToolPolicy.check` 与 task abort 信号 race,超时/取消必释放 | `createApprovalPolicy({ approve(req, signal?) })` 自行 race signal |

> "默认去除所有审查以保证长运行"=不传 `toolPolicy` 即得;长跑稳定性靠默认开启的压缩 + context-editing。

> **durable 挂起/恢复的诚实边界**：`durableApproval` + `CheckpointStore` 提供的是**审批边界的 human-in-the-loop 挂起/恢复 + exactly-once 续跑 + 跨副本恢复**，**不是**通用确定性重放 workflow 引擎；resume token 是单因子 bearer，高风险场景请在 `resume` 外包自己的授权。完整保证/不保证清单见 [`docs/durability-boundary.md`](docs/durability-boundary.md)。

---

## 12. 示例与自检

`src/examples/`（运行需网关可达，url-image/advanced 会起本地端口）：

```bash
npm test                              # vitest 单测+集成（mock brain，含真实 stdio MCP server），CI 友好
npm run smoke                         # mock brain e2e（循环/工具/session/续聊）
npx tsx src/examples/live.ts          # 真实网关：工具调用 + 续聊
npx tsx src/examples/features.ts      # 流式 / 图片(base64) / SQL / blocked
npx tsx src/examples/advanced.ts      # reasoning流 / subagent / team / HTTP-SSE
npx tsx src/examples/url-image.ts     # URL 图片（本地起静态服务供图）
npx tsx src/examples/team-bugs.ts     # 两个不同基础提示的 agent 讨论代码 bug
npx tsx src/examples/mcp-demo.ts      # 真实 stdio MCP server：模型发现→校验→调用→dispose
```

全部应打印 `... OK ✅`。改网关地址：编辑各示例顶部的 `model.baseUrl`。

### 测试按维度组织

`test/` 一个文件一个维度，覆盖矩阵见 [`test/COVERAGE.md`](test/COVERAGE.md)（brain / tools / tasks / sessions /
streaming / compaction / multiagent / mcp / server / concurrency / primitives）。每个发布版本都须保持全维度绿。
确定性单测用 mock brain（离线）；真实网关行为由 `src/examples/*` 手动验证。

### 构建与发布（npm 包）

源码为 TS（NodeNext, ESM）。构建产出 `dist/`（`.js` + `.d.ts`），`exports` 指向 `dist`：

```bash
npm run build     # tsc -p tsconfig.build.json → dist/
npm test          # 全维度回归（prepublishOnly 会跑 build + test）
```

**发布到 Gitea registry**（已配好：`name=@sema-ai/core`，`publishConfig.registry` 指向 Gitea，`files` 只发 `dist`+文档）：

```bash
# .npmrc（本地，已 gitignore；模板见 .npmrc.example）需带一个有 write:package 权限的 Gitea token
npm run build && npm publish     # prepublishOnly 会自动 build + 全维度 test
```

> ⚠️ 发布前提：Gitea PAT 需要 **`write:package`（+ `read:package`）** scope。仓库现用的 token 只有 org/repo 权限，
> 发布会报 `E401`。在 Gitea「设置 → 应用 → 生成令牌」勾选 package 读写即可。版本遵循 semver；原 vendored 内核 MIT，归属史见 `docs/vendor-history.md`。

---

## 13. vendored 内核与许可

- `src/core` `src/brain` `src/tools` `src/agents` `src/server` `src/engine` `src/prompts`：**本项目编写的蒸馏层**（**~24k 行**非 vendor 非 test，2026-06 已远超早期 ~1.9k）。
- `src/engine/`：first-party 引擎层（llm/loop/harness/session/compaction/execution-env；原 openclaw 蒸馏内核已全量 naturalize，历史见 `docs/vendor-history.md`）。
- 内核未发布 npm（openclaw 内部 private workspace 包），故以 vendored 源码方式纳入。
- vendored 层里"磁盘 session / nodejs-exec / 磁盘 skill"等本蒸馏层不用（`StubExecutionEnv` 代替 exec），可按需裁剪。

---

## 14. 已知限制 / TODO

- **真实 DB**：示例用假 executor；接你们库需把 `query` 换成只读连接（`pgQuery(pool)` 等）。
- **图片**：支持 base64 与 URL（URL 由 runner 进程 fetch）。无图片输出生成。
- **subagent**：当前是**同步委派**（父等子）；并行靠 `team`（fan-out/council）与 `cascade`。push-based 异步委派是后续项。
- **持久化中心**：`SessionStore` 接口（§11）+ durable suspend/resume（`CheckpointStore`，design/45/49）已就绪；具体后端（TiDB）在 `ai-agent-service` 仓实现。
- **brain**：内置 OpenAI 兼容 + Anthropic 两个适配器；其他协议自行实现 `Brain`。

> 历史备注：任务内自动压缩（超长单任务的 turn-boundary 压缩）自 **1.88.0** 起支持且**默认开启**
> （`compaction.withinTask`，§9）；更早版本只在任务之间压缩。

---

### 速查：最小可用清单

```
[ ] 一个 OpenAI 兼容网关 baseUrl + 可达
[ ] Model 对象（id / baseUrl / contextWindow / input）
[ ] new Runner({ brain: createOpenAIBrain(), models })
[ ] 工具：createSqlTool({ query: pgQuery(只读池), allowTables }) 或自定义 defineTool
[ ] createTaskServer({ runner, resolveSpec(服务端注入tools), authorize, corsOrigin }).listen(port)
[ ] 前端 POST /task/stream 消费 SSE：reasoning_delta / text_delta / tool_* / done
[ ] 每任务设 limits.timeoutSec；DB 只读；CORS/鉴权收紧
```

---
> 📍 本 README 是**消费者集成(usage)活文档**。架构/编译/bug/路线图入口见 [`docs/HANDOFF.md`](docs/HANDOFF.md)。
> **如何重验本文仍准**:`grep -c export src/index.ts`(导出面,现 105)、`npm run build && echo $?`(签名编译)、`npm test`(行为,现 3351 passed)、`grep -A3 '"dependencies"' package.json`(依赖仅 typebox+MCP)、`npm view @sema-ai/core version`(已发布版本=`1.150.0`)。最后核实 2026-06-27。
