@sema-agent/server 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +74 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +391 -0
- package/dist/boot/budget-tracing.d.ts +48 -0
- package/dist/boot/budget-tracing.js +86 -0
- package/dist/boot/config-center.d.ts +62 -0
- package/dist/boot/config-center.js +1002 -0
- package/dist/boot/coordinators.d.ts +33 -0
- package/dist/boot/coordinators.js +97 -0
- package/dist/boot/execution-env.d.ts +26 -0
- package/dist/boot/execution-env.js +370 -0
- package/dist/boot/leader.d.ts +27 -0
- package/dist/boot/leader.js +81 -0
- package/dist/boot/reapers.d.ts +53 -0
- package/dist/boot/reapers.js +252 -0
- package/dist/boot/resolve-spec.d.ts +70 -0
- package/dist/boot/resolve-spec.js +1072 -0
- package/dist/boot/runner-deps.d.ts +101 -0
- package/dist/boot/runner-deps.js +343 -0
- package/dist/boot/runtime-caps.d.ts +21 -0
- package/dist/boot/runtime-caps.js +62 -0
- package/dist/boot/session-faces.d.ts +57 -0
- package/dist/boot/session-faces.js +157 -0
- package/dist/boot/shutdown.d.ts +50 -0
- package/dist/boot/shutdown.js +129 -0
- package/dist/boot/stores.d.ts +32 -0
- package/dist/boot/stores.js +361 -0
- package/dist/boot/workflow-orchestration.d.ts +46 -0
- package/dist/boot/workflow-orchestration.js +150 -0
- package/dist/capabilities/scenarios.d.ts +5 -3
- package/dist/capabilities/scenarios.js +5 -3
- package/dist/config-center/apply-effective.js +4 -3
- package/dist/config-lkg.d.ts +2 -1
- package/dist/config-lkg.js +2 -1
- package/dist/config-types.d.ts +47 -7
- package/dist/config.d.ts +30 -13
- package/dist/config.js +562 -387
- package/dist/hooks/hook-llm.js +9 -0
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/attachments.js +2 -2
- package/dist/http/routes/memory-policy.js +3 -3
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/routes/session-sync.js +2 -2
- package/dist/http/routes/sessions.js +2 -2
- package/dist/http/routes/tasks.js +2 -2
- package/dist/http/routes/trace-usage.js +2 -2
- package/dist/http/routes/workflows.js +3 -1
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +25 -5
- package/dist/http/sse-log.js +1 -1
- package/dist/main.js +164 -3799
- package/dist/model-select.d.ts +1 -1
- package/dist/model-select.js +1 -1
- package/dist/plugins/checkpoint-store-sql.d.ts +13 -5
- package/dist/plugins/checkpoint-store-sql.js +10 -3
- package/dist/plugins/local-checkpoint-store.js +8 -2
- package/dist/plugins/remote-env-host.d.ts +2 -1
- package/dist/run-local.js +2 -1
- package/dist/session-titler.d.ts +3 -1
- package/dist/session-titler.js +2 -2
- package/dist/trace/project.js +4 -1
- package/package.json +5 -3
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# 迁移指引(BREAKING 变更)
|
|
2
|
+
|
|
3
|
+
> 版本策略:`@sema-agent/server` 1.x = 快速迭代期,行为破坏性变更可能落在 minor 版本
|
|
4
|
+
> (内部多 AI 协作节奏,当日黑板通告+实解)。**生产部署请锁精确版本**;GA 后 2.0 起严格 semver
|
|
5
|
+
> (BREAKING → major)。本文件只记录会影响存量部署行为的变更;完整清单见 `CHANGELOG.md`。
|
|
6
|
+
|
|
7
|
+
## server 3.0.0 —— BREAKING 四条(2026-07-31)
|
|
8
|
+
|
|
9
|
+
> 上线前的唯一兼容窗口(clay 令「不做兼容」):旧形**直接消失或 fail-loud**,不留静默兼容层。
|
|
10
|
+
> 理由统一:静默兼容会让下游的冒烟测试**测不出问题**,把升级风险推迟到生产;拒启/改键让问题在最早
|
|
11
|
+
> 且最清楚的地方现形。
|
|
12
|
+
|
|
13
|
+
| # | 变更 | 旧形(3.0.0 之前) | 新形 | 你要做什么 |
|
|
14
|
+
|---|---|---|---|---|
|
|
15
|
+
| ① | HTTP 错误体摘 legacy `code` 键 | `{ error, errorCode, code }` 双键 | `{ error, errorCode }` | 读 `body.code` 的消费端改读 `body.errorCode`(同值,一行) |
|
|
16
|
+
| ② | SSE `error` 帧三形归一 | `{code,…}` / `{errorCode,…}` / `{message}` 三种 | 一律 `{ type:"error", errorCode, message }` | 改读 `data.errorCode`;可用 `data.type === "error"` 判帧;workflow 流的错误帧此前**无机器码**,现为 `workflow.stream_error` |
|
|
17
|
+
| ③ | D 族负名 env = fail-loud 墓碑 | `X_DISABLED=true` 生效(负名) | 设了(**任何值**)即拒启,文案指路新名 | 按下表把旧名换成正名 |
|
|
18
|
+
| ④ | `MODEL_ID` 无出厂缺省 | 未设 = 用烤死的内网模型名 | 未设 = boot fail-loud | 显式配 `MODEL_ID=<你的网关真正提供的模型名>`,或改由 sema-registry 控制面下发目录 |
|
|
19
|
+
|
|
20
|
+
**③ 的迁移表**(旧名在场即拒启——值是 `true` 还是 `false` 都一样,因为设 `false` 的人同样以为它还生效):
|
|
21
|
+
|
|
22
|
+
| 🪦 旧名(删掉) | 改设 | 缺省 | 这个旋钮管什么 |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `PROJECT_MEMORY_DISABLED` | `PROJECT_MEMORY_ENABLED=false` | 开 | host lane 项目记忆注入(CLAUDE.md + git 叙事) |
|
|
25
|
+
| `CONFIG_LKG_DISABLED` | `CONFIG_LKG_ENABLED=false` | 开 | 中心 effective 配置的 LKG 落盘(读写双关) |
|
|
26
|
+
| `HOST_BG_DISABLED` | `HOST_BG_ENABLED=false` | 开 | host lane 后台 shell 能力总闸 |
|
|
27
|
+
| `HOST_EXEC_SPOOL_DISABLED` | `HOST_EXEC_SPOOL_ENABLED=false` | 开 | host lane exec 的 spool 形 stdio |
|
|
28
|
+
| `LSP_ENABLED=false`(拆分前用来关 host 腿) | `LSP_HOST_ENABLED=false` | 开 | host lane 本地 LSP。⚠️ `LSP_ENABLED=true` **不受影响**——那是沙箱腿自己的 opt-in |
|
|
29
|
+
|
|
30
|
+
**② 的完整码表**见 `docs/ASSISTANT-WIRE-CONTRACT.md` 附录 A;机器执行面是 `test/error-code-key-gate.test.ts`
|
|
31
|
+
(`src/` 全树的 error 帧 + `src/http/` 全树的 4xx/5xx 错误体,两面都不许漏码)。
|
|
32
|
+
|
|
33
|
+
## core 引擎侧 BREAKING(经 `@sema-agent/core` 依赖传入)
|
|
34
|
+
|
|
35
|
+
这些是 core 的行为翻转,server 通过版本 pin 传导给你的部署。滚版前请读对应条目。
|
|
36
|
+
|
|
37
|
+
### 后台 bash 会话驻留(core 1.269.0,server 1.169.0 起)
|
|
38
|
+
- **变更**:后台 `run_in_background` bash **不再随父 run 结束被杀**——改为 session 驻留;终止锚点=
|
|
39
|
+
session 收尾(server 的 E21 DELETE wiring)+ 引擎 hardShutdown 末端全量收割。
|
|
40
|
+
- **影响**:如果你的集成依赖「父任务结束 → 后台进程自动死」,现在需要显式走 session 收尾或
|
|
41
|
+
`TaskStop`。SIGKILL 路径不可救(内核直杀,记档)。
|
|
42
|
+
- **动作**:审查有无长驻后台 shell 的编排假设;TOC/壳形态退出时靠 SIGHUP→drain 路径收割。
|
|
43
|
+
|
|
44
|
+
### Agent 工具默认后台化(core 1.272.0,server 1.173.0 起)
|
|
45
|
+
- **变更**:挂 background surface 的委派工具(Agent/子代理),**省略 `run_in_background` 参数 = 后台执行**
|
|
46
|
+
(async_launched 即回 + 完成通知);要同步取结果须显式传 `run_in_background: false`。
|
|
47
|
+
- **影响**:舰队/集成里「委派后同步等结果」且省略了该参数的调用点,行为从阻塞变异步。
|
|
48
|
+
- **动作**:同步依赖的委派调用显式加 `run_in_background: false`。
|
|
49
|
+
|
|
50
|
+
### Glob `details.numFiles` 语义翻转(core 1.275.0,server 1.176.0 起)
|
|
51
|
+
- **变更**:`numFiles` = **截断后**返回数(与 Grep 统一);要总数改用新增的
|
|
52
|
+
`totalMatches` + `countIsComplete`。
|
|
53
|
+
- **影响**:只影响直接消费 Glob 工具 `details.numFiles` 当「总数」读的下游(server 本体零消费)。
|
|
54
|
+
- **动作**:把 numFiles 当总数读的地方迁 `totalMatches`。
|
|
55
|
+
|
|
56
|
+
## server 侧兼容性说明
|
|
57
|
+
|
|
58
|
+
- **HTTP 契约**:`/v1/tasks` body 字段=`objective`(必填)+ 可选 `model`(catalog id)/`scenario`/
|
|
59
|
+
`sandboxImageProfile`(仅 k8s lane)。字段名稳定,新增字段一律 additive。
|
|
60
|
+
- **env 旋钮**:全部 ship-dark(缺省=旧行为),半配置一律 fail-loud(不静默降级)——升版不会因为
|
|
61
|
+
没配新 env 而改变现有行为。近期新旋钮:`MEMORY_ENGINE_BACKEND` / `MEMORY_SYNC_*` /
|
|
62
|
+
`WORKFLOW_SIZE_GUIDELINE` / `EXPERIMENTAL_OBSERVER_AGENTS` / sealed-box 托管(boot 自动建本机密钥,
|
|
63
|
+
不改现有 env-NAME 通道)。
|
|
64
|
+
- **镜像默认**:k8s 沙箱默认镜像 2026-07-13 起从 code-full(7.75GB)瘦身为 code-node(0.33GB,
|
|
65
|
+
git+node+python);重环境用 per-task `sandboxImageProfile` 按需选。存量部署显式配了
|
|
66
|
+
`K8S_SANDBOX_IMAGE` 的不受影响。
|
|
67
|
+
- **沙箱运行时装包源默认海外化(2026-07-13 起)**:`SANDBOX_PKG_SOURCE` 缺省从「不注入(吃镜像烤死的
|
|
68
|
+
CN 源)」翻转为 **`global`**(pip/uv/npm/go/rustup/flutter 等官方源,以 pod/sandbox env 压过镜像内
|
|
69
|
+
默认,存量镜像无需重烤)。**中国大陆部署请显式配 `SANDBOX_PKG_SOURCE=cn`**(恢复国内镜像源);
|
|
70
|
+
`SANDBOX_PKG_SOURCE=none`=完全不注入(2026-07-13 之前的 unset 行为,字节级不变)。这是 env 旋钮
|
|
71
|
+
「缺省=旧行为」惯例的唯一例外(产品面向海外分发,官方源是正确缺省)。
|
|
72
|
+
- **内网坐标默认值清理(1.180.0,npm 包卫生)**:`MODEL_GATEWAY_BASEURL` 缺省从内部网关 IP 改为
|
|
73
|
+
`http://127.0.0.1:8000/v1` 占位——**依赖旧缺省的部署必须显式配置**;配了 `OA_ISSUE_TOKEN` 的部署
|
|
74
|
+
现在必须同时给 `OA_ISSUE_BASEURL` 或 `GIT_API_BASEURL`(不再有烤死的内网主机兜底,缺失=启动即错)。
|
package/README.md
CHANGED
|
@@ -132,7 +132,7 @@ The server is configured entirely through environment variables. The most import
|
|
|
132
132
|
| `PORT` | `8090` | HTTP listen port |
|
|
133
133
|
| `BIND_HOST` (alias `HOST`) | see note | Listen address. An explicit value **always wins**. Default: `127.0.0.1` when the write face is unauthenticated (`ALLOW_UNAUTHED_WRITES=true` **and** no service token configured), otherwise all interfaces — deployments with a token are unaffected. |
|
|
134
134
|
| `MODEL_GATEWAY_BASEURL` | `http://127.0.0.1:8000/v1` | OpenAI-compatible gateway base URL (without `/chat/completions`) |
|
|
135
|
-
| `MODEL_ID` |
|
|
135
|
+
| `MODEL_ID` | **required** | Default model id — **no factory default since 3.0.0**. Unset ⇒ the server refuses to boot with a message naming the knob (the old baked-in default was an internal-only model name, so every external deployment failed later and further from the cause: a gateway `400` plus a cascade of title-hook warnings). Set it to whatever model name your gateway serves, or supply the catalog via the sema-registry control plane |
|
|
136
136
|
| `MODEL_API_KEY` | — | Gateway API key (optional) |
|
|
137
137
|
| `SERVICE_AUTH_TOKEN` | — | Callers must send `Authorization: Bearer <token>` |
|
|
138
138
|
| `DB_BACKEND` | `local`* | `mysql` (any MySQL-protocol DB: MySQL/TiDB/MariaDB; `tidb` alias) / `pg` (PostgreSQL) / `local` (file-backed, no DB) / `memory` (explicit in-memory: nothing survives a restart, durable-runs faces 501). *Bare boot (no DB env at all) defaults to `local` so a single-user machine keeps its runs across restarts; any SQL signal (`SESSION_BACKEND` or `TIDB_/MYSQL_/PG_HOST`) keeps the `mysql` engine default, and `REQUIRE_PRINCIPAL=true` bare boots stay `memory` (the local file store has no tenant isolation — a warning says so). A DEFAULT-derived `local` that cannot create its data root degrades to memory with a warning + the `store_backend_degraded` gauge; an EXPLICIT `DB_BACKEND=local` fails loud instead. Setting `mysql`/`pg` explicitly also switches sessions to durable |
|
package/README.zh-CN.md
CHANGED
|
@@ -125,7 +125,7 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
|
|
|
125
125
|
| `PORT` | `8090` | HTTP 监听端口 |
|
|
126
126
|
| `BIND_HOST`(兼容 `HOST`) | 见说明 | 监听地址。显式值**恒生效**。缺省:写面无鉴权时(`ALLOW_UNAUTHED_WRITES=true` **且**未配任何 service token)= `127.0.0.1`,否则全接口——配了 token 的部署不受影响。 |
|
|
127
127
|
| `MODEL_GATEWAY_BASEURL` | `http://127.0.0.1:8000/v1` | OpenAI 兼容网关地址(不带 `/chat/completions`) |
|
|
128
|
-
| `MODEL_ID` |
|
|
128
|
+
| `MODEL_ID` | **必填** | 缺省模型 id ——**3.0.0 起无出厂缺省**。未设 = 启动即失败并指路该旋钮(旧的烤死缺省是内网模型名,外部部署必炸且炸在离根因最远处:网关 `400` + 标题 hook 连环告警)。填你的网关真正提供的模型名,或改由 sema-registry 控制面下发目录 |
|
|
129
129
|
| `MODEL_API_KEY` | — | 网关 key(可选) |
|
|
130
130
|
| `SERVICE_AUTH_TOKEN` | — | 调用方需带 `Authorization: Bearer <token>` |
|
|
131
131
|
| `DB_BACKEND` | `mysql` | SQL 引擎:`mysql`(任何 MySQL 协议库:MySQL/TiDB/MariaDB;`tidb` 为兼容别名)/ `pg`(PostgreSQL)/ `local`(免 DB 文件持久化)。显式设置 `mysql`/`pg` 时 session 自动转 durable |
|
package/USAGE.md
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# 调用方手册(OA / 智能体 / 你自己)
|
|
2
|
+
|
|
3
|
+
> 一句话心智模型:**你只对一个地址发普通 HTTP 请求——LB `http://<host>:8090`;请求体里只写"要做什么"。**
|
|
4
|
+
> model / tools / prompt / 安全策略由服务端 `resolveSpec` 注入,**永远不要也不能从请求体传**(凭据不进模型/请求体)。
|
|
5
|
+
> 你不"调度 worker",你只发任务;LB + TiDB 自己协调。每个字段都来自当前真实代码。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 0. 启动(`<host>:8090` 就是你跑起来的这个服务——它不存在直到你部署它)
|
|
10
|
+
|
|
11
|
+
> 部署 = 把本仓跑在一台**能连到模型网关 + TiDB** 的机器上;那台机器的 `IP:8090` 就是你的 `<host>:8090`。
|
|
12
|
+
> 该机器需要 **Node 22 / Bun**(路线 A,无 docker)**或** **Docker**(路线 B)——二选一。
|
|
13
|
+
|
|
14
|
+
**路线 A — 不用 docker(单实例,最简单)**
|
|
15
|
+
```bash
|
|
16
|
+
npm install # 或 bun install(依赖全部公开在 npmjs)
|
|
17
|
+
MODEL_GATEWAY_BASEURL=https://<your-gateway>/v1 MODEL_ID=<your-model-name> \
|
|
18
|
+
DB_BACKEND=mysql MYSQL_HOST=… MYSQL_PORT=6000 MYSQL_USER=… MYSQL_PASSWORD=… MYSQL_DATABASE=sema_server \
|
|
19
|
+
npm run dev # 或 bun run src/main.ts → http://<本机IP>:8090
|
|
20
|
+
```
|
|
21
|
+
自己用 systemd/pm2 守护即可;无 LB、无 compose。多实例集群再走路线 B。
|
|
22
|
+
|
|
23
|
+
**路线 B — docker(最小,单 worker,内存模式,不需要数据库;只支持同步 `/v1/tasks`)**
|
|
24
|
+
```bash
|
|
25
|
+
cd sema-server
|
|
26
|
+
MODEL_GATEWAY_BASEURL=https://<your-gateway>/v1 MODEL_ID=<your-model-name> \
|
|
27
|
+
SESSION_BACKEND=memory \
|
|
28
|
+
docker compose up --build
|
|
29
|
+
# 起来后 OA 只认 http://<host>:8090(LB)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**生产(N worker 集群,需 MySQL 协议库——MySQL/TiDB 均可;支持异步 `/v1/runs` + 断线重连)**
|
|
33
|
+
```bash
|
|
34
|
+
IMAGE_REGISTRY=<registry-namespace> \
|
|
35
|
+
DB_BACKEND=mysql MYSQL_HOST=… MYSQL_USER=… MYSQL_PASSWORD=… MYSQL_DATABASE=sema_server \
|
|
36
|
+
MODEL_GATEWAY_BASEURL=… MODEL_ID=<your-model-name> \
|
|
37
|
+
docker compose up --build --scale service=4 # 4 个无状态 worker,nginx LB 扇发,TiDB 协调
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
> 拓扑:LB(入口)→ N 个一样的 worker → 共享 SQL 库(数据中心;MySQL 协议或 PG)+ 模型网关。无中央调度器,跨 worker 靠 DB 协调。
|
|
41
|
+
|
|
42
|
+
**可选 — 多网关 failover + Anthropic 路由(消除单网关 SPOF)**
|
|
43
|
+
```bash
|
|
44
|
+
# ① 同协议冗余:主网关挂(连不上/上游开始前就失败)→ 按序切到备网关(同一 model id)
|
|
45
|
+
MODEL_GATEWAY_BASEURL=http://gw-a:8000/v1 MODEL_GATEWAY_FALLBACK_URLS=http://gw-b:8000/v1,http://gw-c:8000/v1
|
|
46
|
+
# ② 云 Anthropic 路由:provider="anthropic" 的模型走云 /v1/messages(带 prompt 缓存断点),其余走本地网关
|
|
47
|
+
ANTHROPIC_API_KEY=sk-ant-… # 可选:ANTHROPIC_BASEURL / ANTHROPIC_VERSION / ANTHROPIC_CACHE_BREAKPOINTS=true
|
|
48
|
+
```
|
|
49
|
+
- 两个变量都不设 = 和以前**逐字节一致**(单网关)。
|
|
50
|
+
- **failover ≠ Anthropic↔vLLM**:failover 给所有 brain 发**同一个 model**(同协议同 id 的冗余);云↔本地是**按 `model.provider` 路由**的选择,不是故障转移(两者 model id/参数不同,不能透明互切)。设 `MODEL_PROVIDER=anthropic` + `MODEL_ID=claude-…` 让整个服务走 Anthropic。
|
|
51
|
+
- **缺省推断**:`MODEL_PROVIDER` **未设**、但显式配了 Anthropic 协议 base URL(`ANTHROPIC_BASE_URL` 或 `ANTHROPIC_BASEURL`)**和**对应凭证(`ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`)时,缺省自动判 `anthropic` 并打一行启动 warn(`model_provider_inferred`)——纯净机直连 Anthropic 兼容上游不再需要显式第七个键。显式 `MODEL_PROVIDER` 恒赢;只给 base URL 或只给凭证不推断;两者皆无 = `gateway`,与以前逐字节一致。
|
|
52
|
+
- 启动日志的 `brain` 字段会回显当前组合(`failover` / `anthropicRoute` / `anthropicCacheBreakpoints`)。详见 `design/15`。
|
|
53
|
+
|
|
54
|
+
**可选 — 韧性栈:分级超时 + 断路器(core 1.38,叠在 failover 之下)**
|
|
55
|
+
```bash
|
|
56
|
+
# ③ 分级超时:任务级 timeoutSec 之下补两段,各记为可重试的 [network] → 触发重试/断路器/failover
|
|
57
|
+
MODEL_CONNECT_TIMEOUT_MS=8000 # fetch 迟迟不返响应头(网关连不上)→ 中止
|
|
58
|
+
MODEL_FIRST_TOKEN_TIMEOUT_MS=30000 # SSE 已开但迟迟不吐第一个 delta(网关 hang);reasoning 的首个 thinking 也算首 token
|
|
59
|
+
MODEL_IDLE_TIMEOUT_MS=20000 # (1.40.1)出过 token 后中途卡死:每个 delta 重置,静默超时→中止(补 first-token 只管首字)
|
|
60
|
+
# ④ 断路器:主网关连败 N 次即"开路"→ 快速失败,让 failover 立刻切备(不再逐个等超时)
|
|
61
|
+
MODEL_CIRCUIT_BREAKER=true MODEL_CB_FAILURE_THRESHOLD=5 MODEL_CB_COOLDOWN_MS=30000
|
|
62
|
+
```
|
|
63
|
+
- **全部默认关**(超时=0、断路器=false)→ 不设就和以前**逐字节一致**。
|
|
64
|
+
- 断路器**只在配了 `MODEL_GATEWAY_FALLBACK_URLS`(≥2 路)时才有意义**——它的价值是"开路即快速失败 → failover 立刻切备";单网关下它是 no-op(启动日志 `circuitBreakerNoop` 会提示)。只 `network/server/rate_limit` 计入连败,`auth`/`invalid_request` 不计(坏 key 熔断整网关无意义)。备用网关(最后一路)不套断路器。
|
|
65
|
+
- 断路器状态:配了 `SESSION_BACKEND=tidb` 时自动用**跨副本共享态**(TiDB `circuit_breaker` 表,写穿+刷新最终一致),否则进程内 Map。启动日志 `breakerState` 字段回显 `shared(tidb)`/`in-process`/`off`。详见 `design/27`。
|
|
66
|
+
- **大工具结果落盘**(core 1.47/1.49):单条工具结果 > ~20000 字符时 core 把全文移出上下文、只留预览+ref,模型用 `read_tool_result` 按需分页回取。配了 TiDB 时自动用**durable `tool_result` 表**(跨副本 wake 仍能取回全文;否则 core 进程内默认 = 跨副本 wake 取不到→降级到预览,不崩)。`TOOL_RESULT_TTL_SEC`(默认 86400)按 TTL 回收(要 ≥ run 可恢复期)。启动日志 `toolResultStore` 回显 `shared(tidb)`/`in-process`。
|
|
67
|
+
|
|
68
|
+
**可选 — 成本计量 + 预算闸(core 1.37)**
|
|
69
|
+
```bash
|
|
70
|
+
# 每任务成本/token 上限(运维 CEILING)。调用方可在 body 传更小的 maxCostUsd/maxTokens,但会被夹到这个上限以下。
|
|
71
|
+
MAX_TASK_COST_USD=0.50 MAX_TASK_TOKENS=200000 # 0/不设 = 不限。超限 → 任务 failed + errorCode budget.*
|
|
72
|
+
# 每 principal 跨任务累计成本配额(滚动窗口)。某人窗口内累计花费超顶 → 下一个任务被拒(429 + retry-after)。
|
|
73
|
+
MAX_PRINCIPAL_COST_USD=5.00 COST_QUOTA_WINDOW_SEC=86400 # 0/不设 = 不限;默认窗口 1 天
|
|
74
|
+
# 降级到更便宜的模型(而非直接失败)。MODEL_DEGRADE_TO = 目录里的便宜模型名,两种触发可同时开:
|
|
75
|
+
MODEL_DEGRADE_TO=deepseek-v4-flash
|
|
76
|
+
MODEL_DEGRADE_AT_COST_FRACTION=0.7 # ① 近预算(1.40):累计成本到 0.7×maxCostUsd 切(需任务有 cost ceiling)
|
|
77
|
+
MODEL_DEGRADE_REACTIVE=true # ② 反应式(1.39):主模型 rate_limit/breaker-open 时切(brain 级,最外层)
|
|
78
|
+
MODEL_DEGRADE_ON=rate_limit,breaker_open # 可选,反应式触发器子集;默认两者都开
|
|
79
|
+
```
|
|
80
|
+
- 反应式降级 brain 包在**最外层**(core council #8);fallback brain 用自己的凭据(decorator 清掉主模型的 per-call key 防泄漏给别的 provider)。**坑**:`MODEL_DEGRADE_TO` 最好别和被限流的是同一网关/账号,否则反应式切过去照样撞同一个 rate_limit。
|
|
81
|
+
- **定价怎么设**:`model.cost` 来自 `MODEL_COST_INPUT/OUTPUT/CACHE_READ/CACHE_WRITE`(**USD per 1M tokens**,默认 0 = `costUsd` 读 0)。云模型(如 review-gw 的 deepseek-v4-pro)必须设,否则 spend 恒为 $0;本地自托管(qwen)留 0 即对(无 per-token 外部花费)。字段名是 `costUsd`,**非美元计价的网关要先折算**(如 DeepSeek 官方 CNY ÷ 汇率)。sema-registry 管的模型走 `CenterModel.cost`(中心存价、不存 secret)。
|
|
82
|
+
- 成本计量**自动开**:从 config 的 `model.cost`(per-1M 绝对 USD)注入 `pricing`,core 算出权威的整数 `costMicroUsd`(避免浮点累计误差)。`/metrics` 新增:`model_cost_micro_usd_total{model}`(覆盖所有 brain 调用=主任务+异步+council 子任务的总花费)、`brain_first_token_ms`(网关 hang 早警)、`brain_call_latency_ms`、`tool_calls_total{name,ok}`、`budget_exceeded_total{code}`、`cost_quota_rejected_total`、`degraded_total{reason}`(1.40 降级)。
|
|
83
|
+
- **近预算降级 vs 硬闸**:降级(`MODEL_DEGRADE_TO`,到 `atCostFraction` 切便宜模型)是**撑长**预算、任务仍完成(出口质量下降、发 `task.degraded` 事件可告警);硬闸(`maxCostUsd` 全额)仍在,切了便宜模型还超全额 → `budget.exceeded` 停。
|
|
84
|
+
- **`METRICS_TOKEN`**(可选,只读):设了它,`GET /metrics`+`/metrics/summary` 接受**它或** `SERVICE_AUTH_TOKEN`。**全 fleet 设同一个值** → 控制面(sema-registry)用**一个** token 拉所有 worker 的指标,**无需持有各 worker 的全权 `SERVICE_AUTH_TOKEN`**(不破坏 secret 边界)。即使泄露也只暴露指标(只读)。
|
|
85
|
+
- **`GET /metrics/summary`**(token-gated,同 `/metrics`):`/metrics` 的**精炼 JSON**——`{model, runsActive, tasks{status}, tokensTotal, costUsd, costUsdByModel, taskDurationAvgSec, brainFirstTokenAvgMs, brainCallAvgMs, cacheHitRateAvg, rateLimited, costQuotaRejected, budgetExceeded, degraded, cascade, verifications, councilRuns, toolErrors}`。给**轻量 fleet 看板**用(sema-registry 的 fleet 页按 worker 拉、渲染卡片、按轮询算速率;**不用 Prometheus/Grafana**)。counter 是累计值、histogram 报均值。
|
|
86
|
+
- 预算闸是 core 强制的:`maxCostUsd` pre-call 估算(`budget.precall`,没花钱就拒)+ 流中途取消 + turn 边界(`budget.exceeded`);`maxTokens` 超 → `budget.exceeded`。**review 网关**建议设 `MAX_TASK_COST_USD` 防单任务烧掉共享云 key。
|
|
87
|
+
- **per-principal 累计配额**:用 `AsyncLocalStorage` 把 principal 透传到 cost tracer,所以**council/team 子任务的花费也算到发起人头上**。**配了 `SESSION_BACKEND=tidb` 时自动跨副本共享**(`cost_quota` 表,写后聚合的**原子自增** `micro=micro+delta`,对齐固定窗,最终一致——多副本花费 SUM 到一起、不丢增量);否则 in-memory per-replica 滚动窗(单副本兜底)。启动日志 `costQuota` 字段回显 `shared(tidb)`/`in-process`/`off`。跨副本是最终一致(flush 间隔内峰值可能略超,由**硬 per-task `maxCostUsd` 兜底**)。
|
|
88
|
+
- **`RATE_LIMIT_RPM` 请求限流同样自动跨副本**(`SESSION_BACKEND=tidb` 时,`rate_limit` 表,与配额共用 `WriteBehindCounter`;启动日志 `rateLimit` 回显)。注意:写后聚合 = **软限流**(边界上短暂略超 OK,适合公平/热调用方防护);要**硬合规上限**得另走 CAS/原子计数,不靠写后聚合——和断路器跨副本同款权衡。
|
|
89
|
+
|
|
90
|
+
**可选 — 生成调参透传 + 退化打捞(core 1.59/1.60)**
|
|
91
|
+
```bash
|
|
92
|
+
# 生成参数直透网关(core 1.60 Model.extraBody)。frequency/presence penalty 从源头压退化循环——
|
|
93
|
+
# 是 core 退化检测(安全网)的预防。长链推理/council 易跑飞的部署建议设。
|
|
94
|
+
MODEL_FREQUENCY_PENALTY=0.5 MODEL_PRESENCE_PENALTY=0.3
|
|
95
|
+
MODEL_EXTRA_BODY='{"top_k":40}' # JSON 逃生口(top_k / logit_bias 等;penalty 同名键会覆盖它)
|
|
96
|
+
```
|
|
97
|
+
- **brain 拥有的键永远赢**:`temperature`/`max_tokens` 等放进 `extraBody` 会被 strip + core 警告(`phase:"config"`),不会静默改;auth/content-type/version 头硬锁不可顶替(1.60 安全修复)。
|
|
98
|
+
- **必须静态**:`extraBody` 在 boot 时按固定 env 建一次(稳定键序),**不可逐任务变**,否则破前缀缓存(design/9/31)。未设 → 请求字节级不变。
|
|
99
|
+
- **退化打捞**:模型尾部循环退化时 core 切断,任务 `status:"failed"` + `errorCode:"output.degenerate"`,但 `salvagedOutput` 带着那一退化 turn 的**整段**文本(好内容 + 一段垃圾尾,**当前不裁尾**)。调用方读法:`status==="completed" ? result : salvagedOutput`。penalty 是预防、这是兜底。
|
|
100
|
+
|
|
101
|
+
**可选 — OTLP/HTTP 指标导出(core 1.37 可观测)**
|
|
102
|
+
```bash
|
|
103
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 # 设了才开;周期把指标 POST 到 <endpoint>/v1/metrics
|
|
104
|
+
OTEL_EXPORT_INTERVAL_MS=15000 OTEL_SERVICE_NAME=sema-server
|
|
105
|
+
OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer xxx # 逗号分隔的 k=v(可选,如鉴权头)
|
|
106
|
+
```
|
|
107
|
+
- **零依赖**:不引 OTel SDK(顾及内网镜像 + bun 单文件),直接按 OTLP/JSON 协议把指标注册表 POST 给 collector;Prometheus `/metrics` 不受影响、两者可并存。best-effort——collector 挂了只记日志、绝不影响服务。计数器→Sum(monotonic)、gauge→Gauge、histogram→Histogram(累积桶转成 OTLP 的 per-bucket + `+Inf` 溢出桶)。
|
|
108
|
+
|
|
109
|
+
**可选 — 成本分层 + `@-model`(core 1.24 role map)**
|
|
110
|
+
```bash
|
|
111
|
+
# 便宜模型分层(同网关、换 id):council 的 6 个 lens / 上下文压缩走便宜档,主任务与仲裁走主模型。
|
|
112
|
+
MODEL_ID=<your-model-name> MODEL_CHEAP_ID=<your-cheap-model-name>
|
|
113
|
+
```
|
|
114
|
+
- 调用方可在 `objective` 里 `@<模型名>` 选模型,**只认已配置的名单**(`MODEL_ID` / `MODEL_CHEAP_ID`)——注入不了 `baseUrl`/`apiKey`;无 `@` → 默认模型。
|
|
115
|
+
- **`GET /v1/models`**(带 Bearer)→ `{models:[{name,id,provider,reasoning,vision}],default}`,只含名字/能力、**不含密钥** —— 消费方(如 OA)拿它做 `@` 自动补全下拉的数据源。
|
|
116
|
+
- 不设 `MODEL_CHEAP_ID` → 所有角色 = 主模型(行为不变)。详见 `design/19`。
|
|
117
|
+
|
|
118
|
+
**可选 — 开发者模式 building blocks(core 1.43,默认中立)**
|
|
119
|
+
```bash
|
|
120
|
+
# 给指定角色挂「去品牌的通用编码提示词」CODE_AGENT_PROMPT(工程纪律:理解后改/最小复杂度/危险操作谨慎/验证后报告)。
|
|
121
|
+
MODEL_CODE_ROLES=default,subagent # 不设=全中立;仅这些角色在「任务没自带 systemPrompt」时跑编码提示
|
|
122
|
+
```
|
|
123
|
+
- 经 `RoleSpec.systemPrompt` 挂在**角色**上,非开发角色保持中立、全局默认 `DEFAULT_SYSTEM_PROMPT` 不变;任务自带 `systemPrompt`(或客户端注入)时仍优先。
|
|
124
|
+
- **验证门**(core 1.44,opt-in):请求体带 `verify:true`(可选 `verifyRounds`,夹到 [1,5]、默认 2)→ 任务跑完后由**独立只读对抗 verifier**(`verifier` 角色,默认=主模型)证据强制地"试图 break 它",FAIL 则把 findings 注回同 session 续跑修复→重验,循环到 PASS 或轮数上限。结果带 `verification:{verdict,rounds,findings,evidence}`(`verdict` 看质量,`result`/`status` 仍是实现的)。**仅 `/v1/tasks`(同步)与 `/v1/runs`(异步)**——`/v1/tasks/stream` 不支持(多轮非单流,请求 verify 会 400)。verifier 工具默认 = 实现任务工具滤掉 `effect:"write"`(只读边界)。`/metrics` 加 `verifications_total{verdict}`。
|
|
125
|
+
- **记忆(design/138 文件记忆引擎,2026-07-08 起唯一记忆面)**:core 注入式文件引擎——任务开始时 materialize 记忆目录(`MEMORY_ENGINE_DIR`,默认 `~/.ai-agent`),模型用**普通文件技能**读写记忆(CC `# Memory` 指令 + 派生索引;无 remember/recall 工具),任务边界 harvest 门(secret/cap 扫描)提交。单用户默认开,`MEMORY_ENGINE=off` 显式关;多租户恒关(文件基座无租户隔离,fail-closed)。旧 SQL 记忆面(`MEMORY_BACKEND`/`EMBEDDING_*`/`MEMORY_READ_LIMIT`/去重/向量检索、`GET/DELETE /v1/memory` 与 session memory 写 verb)已退役,数据不迁移——升级后对库跑一次 `scripts/drop-memory-tables.sql`。`body.memoryWrite:false` 仍是每请求只读开关(harvest 不提交)。
|
|
126
|
+
|
|
127
|
+
**可选 — 模型级联 cascade(core 1.45,opt-in)**
|
|
128
|
+
```bash
|
|
129
|
+
# 模型阶梯(便宜→强)。请求体带 cascade:true → 先跑便宜档,门没过(默认=没 completed,即便宜档失败)就升级到强档。
|
|
130
|
+
MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheapest→strongest;不设=cascade 不可用
|
|
131
|
+
```
|
|
132
|
+
- 结果带 `cascadeOutcome:"passed"|"exhausted"` + `escalated`/`finalRung`/`attempts[]`。`/metrics` 加 `cascade_total{outcome}`。
|
|
133
|
+
- **每档用自己的上游 key**:从 sema-registry 模型的 `apiKeyEnv`(env 变量**名**,非密钥值)解析——同一网关下不同模型/账号各用各的 key,没配 `apiKeyEnv` 的模型回落到网关 key(`MODEL_API_KEY`)。`baseUrl` 仍由 brain 层统一持有(非目标)。启动日志 `perModelKeys=N` 报有几个模型带了自己的 key。
|
|
134
|
+
|
|
135
|
+
**嵌入形契约(1.309+;桌面宿主把引擎作为依赖内嵌启动的正门)**
|
|
136
|
+
- 入口:`import "@sema-agent/server/main"`(exports 正门;此前宿主只能用 node_modules 路径字符串定位
|
|
137
|
+
`dist/main.js`,那是未承诺的内部路径)。语义承诺三条:**import 即 boot**(副作用模块形,无需调用);
|
|
138
|
+
**不读 `process.argv`**(宿主可用 argv 传自己的标记,`ps` args 判据不受干扰);`./package.json` 可读。
|
|
139
|
+
- `/health` 身份字段承诺:`pid`(= 引擎进程)与 `dataRoot`(= 生效数据根,解析恒回退 `~/.ai-agent`,
|
|
140
|
+
与 DB_BACKEND 无关)**恒在**——宿主用它们验证「这个端口上的 /health 是不是我起的那个引擎」。
|
|
141
|
+
钉:`test/health-identity-contract.test.ts`。
|
|
142
|
+
- 数据驻留提示:`DB_BACKEND=local` 下显式 `SESSION_BACKEND=memory` 会被收编为 **durable(local)**
|
|
143
|
+
(1.292+ 裸 boot 默认 durable;/health 的 `sessionBackend` 报 `durable(local)`)——session 行落盘在
|
|
144
|
+
数据根下,清数据/隐私预期要按「sessions 在 engine-data 里」来做,不要按「只在内存」。
|
|
145
|
+
- 多实例边界:同一数据根同时只允许一个实例(BootLock 独占,第二个进程拒启;死主自愈含 SIGKILL)。
|
|
146
|
+
`DB_BACKEND=memory` 并非全无盘:workflow 相关账本仍挂数据根下——多个 memory 形引擎**不要共用**
|
|
147
|
+
数据根/HOME(1.309 起并发 boot 不再拒启,但账本内容级共享仍不受支持)。
|
|
148
|
+
|
|
149
|
+
**监听绑址(1.306+,[1934])—— 桌面/单机形请注意**
|
|
150
|
+
- `BIND_HOST`(兼容 `HOST`)= 监听地址;显式设置**恒生效**(要在无鉴权下对外暴露,显式写
|
|
151
|
+
`BIND_HOST=0.0.0.0` 即可)。
|
|
152
|
+
- **缺省自动收窄**:当写面无鉴权时(`ALLOW_UNAUTHED_WRITES=true` 且**完全没有**
|
|
153
|
+
`SERVICE_AUTH_TOKEN`/TOKENS 名录)⇒ 自动绑 `127.0.0.1`,boot 日志点名原因。理由:该形常配
|
|
154
|
+
`REMOTE_EXEC=host`(用户真机、非沙箱),绑全接口=同网段任何人可无鉴权提交任务并执行(1.305 及以前
|
|
155
|
+
`HOST` env **零消费**、恒绑所有接口——沉默陷阱,已修)。
|
|
156
|
+
- 配了凭证的部署(云形/k8s/compose)缺省**不变**(全接口),既有部署零影响。
|
|
157
|
+
|
|
158
|
+
**workspace 浏览面(1.299+,#3)—— 任务产出的浏览/预览/打包下载**
|
|
159
|
+
- 四只读端点(owner 门=session 面同款;`:key=latest`=最新快照):
|
|
160
|
+
`GET /v1/sessions/:id/workspace`(列快照)/ `…/workspace/:key/tree`(文件树,`{path,hash,size?}`)/
|
|
161
|
+
`…/workspace/:key/file?path=`(单文件预览,≤`WORKSPACE_FILE_MAX_BYTES` 默认 8MiB,超限 413
|
|
162
|
+
`workspace_file_too_large` 信封带 `{sizeBytes,limit}`)/ `…/workspace/:key/archive`(流式 tar 下载)。
|
|
163
|
+
- 建在 E19 快照店上(引擎每轮自动快照工作树)——零新存储;无快照店(rewindFiles 未接线)=501,
|
|
164
|
+
探测 `capabilities.workspace`(对象=`{browse,archive,maxFileBytes}`/`false`)。看的是**每轮结束的
|
|
165
|
+
定格**,不是运行中的活工作区(实时浏览=v2 另立)。
|
|
166
|
+
|
|
167
|
+
**可选 — SendUserFile 文件直链(把沙箱/主机里的文件发给用户,S3/MinIO 双轨)**
|
|
168
|
+
```bash
|
|
169
|
+
# 开闸 = 对象存储三键(与 workspace/snapshot 面共用同名 env;三键任一缺席 → 工具不挂载,行为不变)
|
|
170
|
+
MINIO_ENDPOINT=http://minio.internal:9000 # 服务端上传走的内网 S3/MinIO 端点(凭据不出服务端)
|
|
171
|
+
MINIO_ACCESS_KEY=… MINIO_SECRET_KEY=… # 可选 MINIO_REGION;外接 S3 也可只给 S3_ENDPOINT(见下)
|
|
172
|
+
# 公网面(签发的链接用户浏览器要直连拉取;不设 → 工具挂载但签发时报错并提示此键)
|
|
173
|
+
S3_PUBLIC_ENDPOINT=https://files.example.com # 用户可达的 S3/MinIO 公网 base(反代必须透传 Host——SigV4 绑定 Host)
|
|
174
|
+
S3_PUBLIC_BUCKET=sema-public # 永久轨匿名 GET 桶(默认 sema-public)
|
|
175
|
+
SESSION_SNAPSHOT_BUCKET=session-snapshots # 限时轨私桶(与 snapshot 面共用;sendfile/ 前缀内隔离)
|
|
176
|
+
SEND_USER_FILE_URL_TTL=0 # 缺省 ttl 秒:0=永久(默认);1..604800=限时签名链接;非法值回落 0
|
|
177
|
+
SEND_USER_FILE_SANDBOX_PUT_ENDPOINT=… # 可选:沙箱直传 PUT 的端点(默认=S3_PUBLIC_ENDPOINT;
|
|
178
|
+
# k8s 集群内 pod 可指内网 MinIO 省公网带宽)
|
|
179
|
+
```
|
|
180
|
+
- **词表优先级**:内网端点 `MINIO_ENDPOINT` 优先,`S3_ENDPOINT` 是外接 S3 兼容位(内网/公网同址场景可只给它,同时充当公网面);公网端点 `S3_PUBLIC_ENDPOINT` 优先,缺席回落 `S3_ENDPOINT`。空串一律按未设处理。
|
|
181
|
+
- **🔴 云形(`DB_BACKEND=mysql|pg`)快照 blob 强制对象存储(1.295+,clay 拍)**:缺 MinIO 三键 ⇒ **boot 拒启**——单行 SQL blob 写会撞 TiDB `txn-entry-size-limit`(默认 6MiB,真库实测矮墙)/ mysql 协议 `max_allowed_packet`,大字节归对象存储(与 D-1 附件面同裁定)。单机/测试台显式逃生:`SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true`(SQL 店此时带 per-blob 帽,tidb 默认 6MiB,超限 PUT 413 `blob_too_large_for_sql`;帽可用 `SNAPSHOT_BLOB_SQL_MAX_BYTES` 按部署真实限值覆写,两方言生效;pg 默认无帽)。
|
|
182
|
+
- **🔴 公桶策略必须 GetObject-only**:`mc anonymous set download` 会**连带打开 ListBucket**——匿名 `GET /<bucket>/?list-type=2` 能枚举全部不可猜 key,能力链接设计即告失效。正确姿势=`mc anonymous set-json`,policy 只含 `Action:["s3:GetObject"]` on `arn:aws:s3:::<bucket>/*`(验证:对象 GET 200、桶 LIST 403)。
|
|
183
|
+
- **两条轨**:ttl=0(默认)→ 匿名 GET 公桶下 `uuidv7/<name>` 不可猜 key,链接永久、可回收(删对象);0<ttl≤7 天 → 私桶 + SigV4 限时签名链接(7 天是 SigV4 物理上限,更久用 ttl=0)。
|
|
184
|
+
- **执行 lane 与租户门**:e2b/k8s 沙箱 lane 任意租户可用(沙箱文件系统=租户边界,沙箱内 `curl -T` 直传、字节不中转、凭据不进沙箱);host/ssh lane 仅单用户部署(`REQUIRE_PRINCIPAL` 未开)时挂载。
|
|
185
|
+
- **web 侧感知**:`GET /v1/capabilities` 透出 `sendUserFile`(READY 语义:工具真挂载**且**公网端点在场——调用真能成功才 yes)与 `s3PublicEndpoint`(渲染用公网 base;绝不透出密钥/内网端点)。
|
|
186
|
+
|
|
187
|
+
**可选 — 接 sema-registry(中心化模型/角色/团队配置)**
|
|
188
|
+
```bash
|
|
189
|
+
# 新名(orchestrator 现注入);旧名 CONFIG_CENTER_* 仍兼容(dual-read,新名优先、旧名回退)
|
|
190
|
+
SEMA_REGISTRY_URL=http://<sema-registry-host>:3100 # 启动拉 GET /api/config/effective(Bearer+ETag),覆盖 env 兜底
|
|
191
|
+
SEMA_REGISTRY_TOKEN=<SERVICE_PULL_TOKEN 的值> # 取自 sema-registry 主机 .env;只读拉取令牌
|
|
192
|
+
SEMA_REGISTRY_DRY_RUN=true # 安全灰度:只 LOG 中心配置 vs env 推导的差异,不 apply
|
|
193
|
+
SEMA_REGISTRY_WORKER=<worker名> # 可选:拉取 /effective?worker=<名> 取该 worker 的 roster(reconciler 按 worker 注);不设=全局 roster(向后兼容)
|
|
194
|
+
```
|
|
195
|
+
- 中心**空/未发布** → `applyEffective` 回落 env + 内建 teams 并 warn `config_center_unpublished`,**不影响在跑的服务**(接了也安全)。
|
|
196
|
+
- **灰度姿势**(sema-registry AI 建议):先 `SEMA_REGISTRY_DRY_RUN=true` 起一轮,看日志 `sema_registry_dry_run`(中心给的 models/roles/teams + 会否覆盖 default、per-model apiKeyEnv)对得上 env 再去掉该 flag 真正 apply。
|
|
197
|
+
- 拉取**只读、只取逻辑配置**(模型名册/角色/团队);密钥/网关仍在本服务 env(中心只发 env-**名** 引用,不发密钥值)。models/roles 改动需重启生效,teams 60s 热刷。回滚=去掉 `SEMA_REGISTRY_URL` 即纯 env。
|
|
198
|
+
- **仅 `/v1/tasks`(同步)+ `/v1/runs`(异步)**——`/v1/tasks/stream` 不支持(多次尝试非单流,400)。与 `verify` **互斥**(同时给 → 400)。
|
|
199
|
+
- 成本上界 = 任务的 `maxCostUsd`(防冷重跑税)。**三条 core 警示**:① 每档**冷重跑**重付输入成本(便宜档常过才划算);② 门收到**未脱敏**输出(自定义门转发外部 verifier 要脱敏);③ **写工具会跑 N 次**——**只用于只读/幂等任务**(每次升级整任务重跑)。开放式任务(找全 bug/文笔)没有可判定 oracle、级联会空转,那种用 `team`(广度对抗)而非级联(深度阶梯)。
|
|
200
|
+
|
|
201
|
+
**布尔旋钮的取值与极性(运维必读)**
|
|
202
|
+
|
|
203
|
+
布尔 env **只认 `true` / `false` 两个字面量**。写成 `1` / `yes` / `TRUE` ⇒ 该旋钮退回自己的缺省值,并在启动时
|
|
204
|
+
报一条 `config_env_invalid_using_default`(`env` + `raw` 字段)。以前这是**静默**退默认的,`LSP_ENABLED=1`
|
|
205
|
+
这种在人眼里是"开"、在代码里是"关"的写法查不出来。
|
|
206
|
+
|
|
207
|
+
缺省值**推不出来**——同一个 `*_ENABLED` 后缀底下有三种极性。所以 `LOG_LEVEL=debug` 时服务每个旋钮打一行
|
|
208
|
+
`config_knob_polarity`:`knob` / `polarity` / `value` / `source`,直接告诉你这台机器上每个开关此刻是什么、为什么:
|
|
209
|
+
|
|
210
|
+
| polarity | 未设时 | 含义 |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `opt-in` | **关** | 显式 `=true` 才开(实验面、有代价的面、危险面) |
|
|
213
|
+
| `opt-out` | **开** | 显式 `=false` 才关(缺省即最佳实践,给逃生舱) |
|
|
214
|
+
| `posture` | 取决于 `REQUIRE_PRINCIPAL` | 单用户 turnkey(未设 `REQUIRE_PRINCIPAL=true`)且基建就绪 ⇒ 开;多租户 ⇒ 关。显式字面量永远压过姿势推导 |
|
|
215
|
+
|
|
216
|
+
`source` 说明这个值从哪来:`env`(显式设了)/ `default`(缺省)/ `posture`(姿势推导)/ `legacy-env`(命中了
|
|
217
|
+
下面的兼容旧名)。表覆盖的是**本次加载中活着的旋钮**:挂在未激活通道/形状上的旋钮(非 k8s 通道下的
|
|
218
|
+
`K8S_INSECURE_TLS`、未设 `MODEL_DEGRADE_TO` 时的 `MODEL_DEGRADE_REACTIVE` 等)不出行——没有行=「在这份
|
|
219
|
+
配置里不生效」,不是「关」。
|
|
220
|
+
|
|
221
|
+
**改名(server 3.0.0 起旧名=fail-loud 墓碑:设了旧名直接拒启,错误文案指路新名。选拒启不选静默
|
|
222
|
+
忽略——静默会让升级部署的旧开关名义在、实际归默认,冒烟测不出来)**
|
|
223
|
+
|
|
224
|
+
| 🪦 墓碑旧名(设了即拒启) | 改设这个新名 | 缺省 | 说明 |
|
|
225
|
+
|---|---|---|---|
|
|
226
|
+
| `PROJECT_MEMORY_DISABLED` | `PROJECT_MEMORY_ENABLED=false` | 开 | host lane 项目记忆注入(CLAUDE.md + git 叙事) |
|
|
227
|
+
| `CONFIG_LKG_DISABLED` | `CONFIG_LKG_ENABLED=false` | 开 | 中心 effective 配置的 LKG 落盘(读写双关) |
|
|
228
|
+
| `HOST_BG_DISABLED` | `HOST_BG_ENABLED=false` | 开 | host lane 后台 shell 能力总闸 |
|
|
229
|
+
| `HOST_EXEC_SPOOL_DISABLED` | `HOST_EXEC_SPOOL_ENABLED=false` | 开 | host lane exec 的 spool 形 stdio(关掉=回退管道旧形) |
|
|
230
|
+
| `LSP_ENABLED=false`(拆分前用来关 host 腿那半) | `LSP_HOST_ENABLED=false` | 开 | 见下 |
|
|
231
|
+
|
|
232
|
+
四个旧名都是"负名"(`X_DISABLED`),读的时候要双重否定;新名一律正向 + 缺省显式。
|
|
233
|
+
🪦 **墓碑语义**:旧名**在场即拒启**——值是 `true` 还是 `false` 都一样(设 `false` 的人同样以为它还生效),
|
|
234
|
+
错误文案直接给出该改成什么。所以不存在"两个名字同时设"的状态,也不存在"旧名还在悄悄生效"的状态。
|
|
235
|
+
|
|
236
|
+
**`LSP_ENABLED` 一分为二**:它原来同时驱动两条腿,而且两腿缺省相反——沙箱腿缺省**关**(要烤好的
|
|
237
|
+
`sema-code-lsp` 模板),host 腿缺省**开**(只要 PATH 上有 language server,没有就优雅退回 grep/read)。
|
|
238
|
+
现在沙箱腿仍是 `LSP_ENABLED`(opt-in),host 腿归 `LSP_HOST_ENABLED`(opt-out)。
|
|
239
|
+
🪦 `LSP_ENABLED=false`(拆分前唯一的 host 腿逃生舱)自 3.0.0 起是墓碑:拒启并指路 `LSP_HOST_ENABLED`。
|
|
240
|
+
`LSP_ENABLED=true` 不受影响——那是沙箱腿自己的 opt-in,语义没变。
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 1. 请求与响应的通用规则
|
|
245
|
+
|
|
246
|
+
**所有 POST 都带:** `-H 'content-type: application/json'`
|
|
247
|
+
|
|
248
|
+
**鉴权 / 身份(两个不同的头,按需):**
|
|
249
|
+
| 头 | 什么时候必须 | 含义 |
|
|
250
|
+
|---|---|---|
|
|
251
|
+
| `Authorization: Bearer <SERVICE_AUTH_TOKEN>` | 设了 `SERVICE_AUTH_TOKEN` 时,所有非 `/health` 请求 | **谁有权调本服务**(OA 后端持有,服务到服务) |
|
|
252
|
+
| `x-agent-principal: user:42` | 设了 `REQUIRE_PRINCIPAL=true` 时 | **代表哪个终端用户**(决定 session 归属 + 记忆隔离;**绝不从 body 取**) |
|
|
253
|
+
|
|
254
|
+
> ⚠️ **Bearer 是"每个请求",不只是 POST。** 配了 `SERVICE_AUTH_TOKEN` 后,**`GET /v1/runs/:id`、`GET /v1/runs/:id/events`(SSE)、`/metrics`** 等所有非 `/health` 路由都要带 `Authorization: Bearer`——漏带一律 `401`。下面示例为简洁**省略了 Bearer**,真实调用请逐个补上。
|
|
255
|
+
|
|
256
|
+
**请求体字段(你能传的全部):**
|
|
257
|
+
| 字段 | 必填 | 说明 |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| `objective` | ✅ | 要做什么(自然语言)。**唯一必填。** |
|
|
260
|
+
| `sessionId` | — | 续聊:带上次返回的 `sessionId`,服务端自动 wake 历史 |
|
|
261
|
+
| `images` | — | 图文输入 `[{data,mimeType}|{url}]`(模型需支持 vision) |
|
|
262
|
+
| `attachmentIds` | — | D-1 通用文件上传(1.289+):先 `POST /v1/attachments?name=…`(raw body,content-type=mime)拿句柄,提交时引用 ≤16 个;文件物化到执行环境工作目录 `attachments/` 下,objective 尾部自动追加文件清单(内容不进会话流)。单文件缺省 ≤32 MiB(`ATTACHMENT_MAX_BYTES`);可配 mime 白名单(`ATTACHMENT_MIME_ALLOWLIST` CSV,缺省不限);上传后未引用的按 `ATTACHMENT_UNBOUND_TTL_MS`(缺省 24h)回收。**云形态(tidb/pg)字节本体存对象存储——MinIO 必配**(`MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY`,与快照 lane 同一组变量),未配则附件面 501;local 形走本地文件店。 |
|
|
263
|
+
| `scenario` | — | `default`(默认)/ `code-review`(见 §5)/ `scan`(同 §5 的 repo 只读工具但**中性无框架提示词**——objective+中心下发 skill 全权主导输出,OA 扫描类用)/ **sema-registry 可声明任意新场景**(`{name, toolset: none\|repo-readonly, prompt?}`,组合即配置、能力钉死在部署;restart-to-apply;center 可覆盖内建名,boot 日志 `config_center_scenarios.shadowsBuiltin` 可审计) |
|
|
264
|
+
| `repo` / `council` / `debate` | — | `repo` 为 `code-review`/`scan` 必填;`council`/`debate` 仅 `code-review`,见 §5 |
|
|
265
|
+
| ~~model / tools / prompt~~ | 🚫 | **不接受**——服务端注入 |
|
|
266
|
+
|
|
267
|
+
**`TaskResult`(同步 / `done` 事件里拿到的):**
|
|
268
|
+
```json
|
|
269
|
+
{ "taskId":"…", "sessionId":"0190…", "status":"completed",
|
|
270
|
+
"result":"最终回答文本",
|
|
271
|
+
"blockedReason": null, "errorMessage": null, "errorCode": null,
|
|
272
|
+
"stats": { "turns": 1, "tokens": 123 } }
|
|
273
|
+
```
|
|
274
|
+
| 字段 | 用途 |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `result` | 答案文本 |
|
|
277
|
+
| `sessionId` | 续聊用——下次带回 |
|
|
278
|
+
| `status` | `completed` / `blocked` / `failed` / `timeout` |
|
|
279
|
+
| `errorCode` | 程序化分支:如 `"conflict"`(乐观锁丢失,可重试) |
|
|
280
|
+
| `blockedReason` | `status=blocked` 时:为什么做不了(缺信息/权限) |
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## 2. Hello world——同步发一个任务,拿到答案(最简单)
|
|
285
|
+
```bash
|
|
286
|
+
curl -s http://<host>:8090/v1/tasks -H 'content-type: application/json' \
|
|
287
|
+
-H 'x-agent-principal: user:42' \
|
|
288
|
+
-d '{"objective":"用一句话介绍 TiDB"}'
|
|
289
|
+
# → 200, 阻塞到跑完返回上面那个 TaskResult。result 就是答案。
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## 3. 多轮对话——带回 `sessionId`
|
|
293
|
+
```bash
|
|
294
|
+
curl -s http://<host>:8090/v1/tasks -H 'content-type: application/json' \
|
|
295
|
+
-H 'x-agent-principal: user:42' \
|
|
296
|
+
-d '{"objective":"它和 MySQL 最大的区别是什么?","sessionId":"0190…"}' # ← 同一个 sessionId
|
|
297
|
+
# → 服务端自动 wake 上轮历史。「恢复对话」= 同 sessionId 再发一次,无需调别的接口。
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## 4. 生产姿势——异步(长任务 / 实时 UI / 断线重连,需 TiDB)
|
|
301
|
+
```bash
|
|
302
|
+
# ① 立刻拿 id(不阻塞,后台跑)
|
|
303
|
+
curl -s http://<host>:8090/v1/runs -H 'content-type: application/json' \
|
|
304
|
+
-H 'x-agent-principal: user:42' -d '{"objective":"审查这个 PR …"}'
|
|
305
|
+
# → 202 {"taskId":"…","sessionId":"…","status":"running"}
|
|
306
|
+
# 同一 session 已有活跃 run → 409 {"error":"session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)","activeTaskId":"…"}
|
|
307
|
+
|
|
308
|
+
# ② 实时看进度(SSE,可断线重连:Last-Event-ID 从断点续)
|
|
309
|
+
# 注意:run 的 GET 是 owner 校验的——带上和发起时同一个 x-agent-principal,否则 404
|
|
310
|
+
curl -N http://<host>:8090/v1/runs/<taskId>/events \
|
|
311
|
+
-H 'x-agent-principal: user:42' -H 'Last-Event-ID: 0'
|
|
312
|
+
# SSE 每条三行:
|
|
313
|
+
# id: 1 ← seq,断线重连就把它当 Last-Event-ID 传回
|
|
314
|
+
# event: text ← 事件类型
|
|
315
|
+
# data: {"type":"text","text":"…"} ← 扁平 {type, …字段}
|
|
316
|
+
# 类型:text(按 turn 合并的文本)/ tool_start / tool_end / turn_end / compacted /
|
|
317
|
+
# done(data 里有完整 TaskResult)/ failed
|
|
318
|
+
# 断线后:curl -N …/events -H 'Last-Event-ID: 12' → 从 seq 13 续,不重复
|
|
319
|
+
|
|
320
|
+
# ③ 或者轮询最终结果(同样带 principal)
|
|
321
|
+
curl -s http://<host>:8090/v1/runs/<taskId> -H 'x-agent-principal: user:42' # → {status, result, error?}
|
|
322
|
+
```
|
|
323
|
+
> 红利:②③ 经 LB 落到**任意一个 worker** 都能拿到同一个 run 的事件(数据全在 TiDB)。OA 不用关心是哪个 worker。
|
|
324
|
+
|
|
325
|
+
## 5. 代码评审场景(`scenario:"code-review"`)
|
|
326
|
+
请求体加 `scenario` + `repo`(`owner/name` 或 URL)。三档(成本/质量权衡):
|
|
327
|
+
```bash
|
|
328
|
+
# 直接评审(单 agent,快,默认):
|
|
329
|
+
-d '{"scenario":"code-review","repo":"your-org/your-repo","objective":"审 src/core/session.ts 的并发"}'
|
|
330
|
+
# council(L1 六镜头并行取证 + L3 仲裁,分桶 BUG/DESIGN/QUESTION):加 "council":true
|
|
331
|
+
# debate(再加 L2 平级辩论 + 工具核验,最高精度最贵):加 "council":true,"debate":true
|
|
332
|
+
```
|
|
333
|
+
服务端需配 `GIT_API_BASEURL` + `GIT_API_TOKEN`(只读,服务端持有);缺则 501,缺 `repo` 则 400。建议走异步 `/v1/runs`(council/debate 几分钟级)。
|
|
334
|
+
|
|
335
|
+
## 6. 逐 token 直播(同步流式,连接挂着)
|
|
336
|
+
```bash
|
|
337
|
+
curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
|
|
338
|
+
-H 'x-agent-principal: user:42' -d '{"objective":"…"}'
|
|
339
|
+
# → SSE,每条 data: <原始 TaskEvent>,如 {"type":"text_delta","delta":"…"} … {"type":"done","result":{…}}
|
|
340
|
+
# 注意:这是「同实例逐 token」;跨 worker 断线重连用 §4 的 /v1/runs(turn 粒度)。
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## 7. 其它接口(按需)
|
|
344
|
+
| 端点 | 用途 |
|
|
345
|
+
|---|---|
|
|
346
|
+
| `GET /v1/sessions/<id>` | 审计回溯:当前上下文 + 摘要(owner 校验) |
|
|
347
|
+
| `GET /v1/approvals?owner=user:42` | 高危写审批:**operator** 看待办队列(可按 owner 过滤);非 operator 只看自己的 |
|
|
348
|
+
| `POST /v1/approvals/<id>` `{"decision":"approve"|"deny","reason":"…"}` | 批/否(CAS,重复决议 409);**仅 operator**(非 operator → 403) |
|
|
349
|
+
| `GET /health` | 健康(无需鉴权) |
|
|
350
|
+
| `GET /metrics` | Prometheus 指标(有 token 时需带) |
|
|
351
|
+
|
|
352
|
+
> **operator 鉴权(审批队列)**:`OPERATOR_PRINCIPALS=ops:alice,ops:bob`(CSV)= 谁能当 operator——列任意 owner 待办 + 决议(批/否)。**不设=旧行为**(握 service token 即 operator,向后兼容);设了之后,非名单 principal 列待办只看自己的、且**不能决议**(403,防"请求方批自己的高危操作"绕过 F4 闸)。
|
|
353
|
+
>
|
|
354
|
+
> **parked 后台子代的待办分两个 scope 桶**(durable 审批面,core 1.389 起):父任务显式转发审批范围的常规 ask 落在该范围的 scope 下(可预算);无转发时无人值守拦下的敏感操作 ask 落在按 principal 派生的隔离 scope 下(带缺省 deadline、永不自动放行)。operator 全量列表天然两桶全见;**按 `?owner` 过滤时注意两桶可能不同名**,展示面要两个都查。
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## 8. 三类调用方怎么接
|
|
359
|
+
|
|
360
|
+
**A. OA 系统(服务到服务后端)**
|
|
361
|
+
- OA 后端持有 `SERVICE_AUTH_TOKEN`,每个请求按当前终端用户设 `x-agent-principal: user:<id>`。
|
|
362
|
+
- 短问答 → `/v1/tasks`(同步);长任务/要进度条 → `/v1/runs` + `/events`(异步)。
|
|
363
|
+
- 续聊:把上次 `sessionId` 存在 OA 会话里,下次带回。
|
|
364
|
+
|
|
365
|
+
**B. 智能体客户端直接调(把本服务当一个"委派工具")**
|
|
366
|
+
- 最简单:`POST /v1/tasks`,body `{"objective":"<要委派的子任务>"}`,拿 `result` 当工具输出。
|
|
367
|
+
- 要让 agent 自己审仓库:`{"scenario":"code-review","repo":"…","objective":"…","council":true}`。
|
|
368
|
+
- 多轮:agent 维护 `sessionId` 即可保持上下文。
|
|
369
|
+
|
|
370
|
+
**C. 你自己的简单入口**
|
|
371
|
+
- 就是上面的 `curl`;或跑 `./smoke.sh http://<host>:8090 user:42` 一条命令端到端验。
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## 9. 状态 / 错误码速查
|
|
376
|
+
| 你看到 | 含义 | 怎么办 |
|
|
377
|
+
|---|---|---|
|
|
378
|
+
| HTTP `200` + `status:"completed"` | 成功 | 取 `result` |
|
|
379
|
+
| `status:"blocked"` | agent 主动报卡住 | 看 `blockedReason`,补信息再发 |
|
|
380
|
+
| `status:"failed"` + `errorCode:"conflict"` | 跨实例乐观锁丢失 | 直接重试(幂等) |
|
|
381
|
+
| `status:"timeout"` | 超时 | 拆小任务 / 提高超时(服务端 `limits`) |
|
|
382
|
+
| HTTP `401` | 缺 `Authorization` / 缺 `x-agent-principal`(要求时) | 补头 |
|
|
383
|
+
| HTTP `403` / `404`(session/run) | 不是该 principal 的资源 | 用正确身份 |
|
|
384
|
+
| HTTP `409`(`/v1/runs`) | 同 session 已有活跃 run | 等它完成 / 用返回的 `activeTaskId` |
|
|
385
|
+
| HTTP `429` | 限流 | 看 `Retry-After` 退避 |
|
|
386
|
+
| HTTP `501`(`/v1/runs`) | 内存模式不支持异步 | 配 TiDB(`SESSION_BACKEND=tidb`) |
|
|
387
|
+
|
|
388
|
+
**机器码**:每个 4xx/5xx 响应体都带一个 `errorCode`(与人类文案 `error` 并列),这是**唯一**该拿来做
|
|
389
|
+
程序分支的字段——**别锚 `error` 文案**。前缀族固定:`auth.` / `request.` / `not_found.` / `conflict.` /
|
|
390
|
+
`limit.` / `capability.`(本部署没接这个面)/ `feature.`(开关没开)/ `internal.` / `state.`;未知码按前缀
|
|
391
|
+
兜底永远安全。完整码表见 `docs/ASSISTANT-WIRE-CONTRACT.md` §附录 A。
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ModelUsageTracker, PromptManifestTracker } from "../budget.js";
|
|
2
|
+
import type { ServiceConfig } from "../config.js";
|
|
3
|
+
import { FleetUsageAccumulator } from "../fleet-client.js";
|
|
4
|
+
import { CostQuota } from "../observability/cost-quota.js";
|
|
5
|
+
import type { Logger } from "../observability/logger.js";
|
|
6
|
+
import type { Metrics } from "../observability/metrics.js";
|
|
7
|
+
import type { BreakerStateStore, StoreBackend } from "../plugins/store-backend.js";
|
|
8
|
+
export interface BudgetTracingCtx {
|
|
9
|
+
config: ServiceConfig;
|
|
10
|
+
logger: Logger;
|
|
11
|
+
metrics: Metrics;
|
|
12
|
+
backend: StoreBackend | undefined;
|
|
13
|
+
breakerState: ReturnType<BreakerStateStore["startRefresh"]> | undefined;
|
|
14
|
+
}
|
|
15
|
+
export declare function createBudgetAndTracing(ctx: BudgetTracingCtx): {
|
|
16
|
+
brain: import("@sema-agent/core").Brain;
|
|
17
|
+
pricing: Record<string, import("@sema-agent/core").ModelPricing>;
|
|
18
|
+
counterDegradeHook: (info: {
|
|
19
|
+
table: string;
|
|
20
|
+
kind: string;
|
|
21
|
+
streak: number;
|
|
22
|
+
prevStreak: number;
|
|
23
|
+
error?: string;
|
|
24
|
+
}) => void;
|
|
25
|
+
costQuota: CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota | undefined;
|
|
26
|
+
modelUsageTracker: ModelUsageTracker;
|
|
27
|
+
promptManifestTracker: PromptManifestTracker;
|
|
28
|
+
fleetUsage: FleetUsageAccumulator | undefined;
|
|
29
|
+
fleetLease: import("../fleet-lease.js").FleetLeaseManager | undefined;
|
|
30
|
+
tracer: import("@sema-agent/core").TracerHook;
|
|
31
|
+
sideQueryAccounting: (principal: string | undefined, r: {
|
|
32
|
+
model: string;
|
|
33
|
+
family?: "input-includes-cached" | "input-excludes-cached";
|
|
34
|
+
usage?: {
|
|
35
|
+
input?: number;
|
|
36
|
+
output?: number;
|
|
37
|
+
cacheRead?: number;
|
|
38
|
+
cacheWrite?: number;
|
|
39
|
+
cost?: {
|
|
40
|
+
total?: number;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}) => void;
|
|
44
|
+
toolResultStore: import("../plugins/store-backend.js").ToolResultStoreFull | undefined;
|
|
45
|
+
sessionPolicyStore: import("@sema-agent/core").SessionPolicyStore | undefined;
|
|
46
|
+
fileSnapshotStore: import("../plugins/store-backend.js").ServiceFileSnapshotStore | undefined;
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=budget-tracing.d.ts.map
|